package com.hyt.it.ogt.ks.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.hyt.common.apiInfor.ApiResponse;
import com.hyt.core.base.BaseEntity;
import com.hyt.core.base.BaseServiceImpl;
import com.hyt.core.util.UUIDUtils;
import com.hyt.it.ogt.ks.api.kw.model.CancelSubmitPaperDTO;
import com.hyt.it.ogt.ks.api.kw.model.CandidateAddTimeDTO;
import com.hyt.it.ogt.ks.api.kw.model.CandidateUseTimeDTO;
import com.hyt.it.ogt.ks.api.kw.model.ForceFinishExamDTO;
import com.hyt.it.ogt.ks.config.ConfigManager;
import com.hyt.it.ogt.ks.config.FaceVerifyConfig;
import com.hyt.it.ogt.ks.config.KsBizRunTimeException;
import com.hyt.it.ogt.ks.enums.*;
import com.hyt.it.ogt.ks.enums.log.ApplyCheckState;
import com.hyt.it.ogt.ks.enums.log.CandidateOperatorLogType;
import com.hyt.it.ogt.ks.enums.log.LogEventType;
import com.hyt.it.ogt.ks.exception.KsException;
import com.hyt.it.ogt.ks.feign.KwClient;
import com.hyt.it.ogt.ks.feign.LoginClient;
import com.hyt.it.ogt.ks.feign.model.*;
import com.hyt.it.ogt.ks.feign.model.param.FaceAuthFailParam;
import com.hyt.it.ogt.ks.feign.model.vo.FaceAuthFailVO;
import com.hyt.it.ogt.ks.job.CandidateProcessAccessTimeTask;
import com.hyt.it.ogt.ks.mapper.*;
import com.hyt.it.ogt.ks.model.dto.MobileDeviceNoticeDTO;
import com.hyt.it.ogt.ks.model.entity.*;
import com.hyt.it.ogt.ks.model.vo.*;
import com.hyt.it.ogt.ks.model.vo.login.LoginInfo;
import com.hyt.it.ogt.ks.model.vo.paper.PItemDTO;
import com.hyt.it.ogt.ks.model.vo.paper.POptionDTO;
import com.hyt.it.ogt.ks.model.vo.paper.PTopicDTO;
import com.hyt.it.ogt.ks.mq.CandidateInfoProducer;
import com.hyt.it.ogt.ks.mq.model.CandidateInfoUpdateDTO;
import com.hyt.it.ogt.ks.service.*;
import com.hyt.it.ogt.ks.util.*;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.hyt.it.ogt.ks.enums.BizStatusCode.NOT_EXIST_CANDIDATE;

/**
 * <p>
 * 考生考试过程表 服务实现类
 * </p>
 *
 * @author chengxh
 * @since 2020-06-17
 */
@Slf4j
@Service
public class CandidateProcessServiceImpl extends BaseServiceImpl<CandidateProcessMapper, CandidateProcess> implements ICandidateProcessService {

    @Resource
    private CandidateAnswerMapper candidateAnswerMapper;
    @Resource
    private KwClient kwClient;
    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private CandidateInfoProducer candidateInfoProducer;
    @Resource
    private IExamCacheService iExamCacheService;
    @Resource
    private ICandidateAnswerService iCandidateAnswerService;
    @Resource
    private ICandidatePaperService iCandidatePaperService;
    @Resource
    private PaperTopicMapper paperTopicMapper;
    @Resource
    private ForceItemMapper forceItemMapper;
    @Resource
    private IPaperService iPaperService;
    @Resource
    private CandidateBrowserInfoMapper candidateBrowserInfoMapper;
    @Resource
    private ICandidateAnswerBackupService iCandidateAnswerBackupService;
    @Resource
    private CandidateProcessAccessTimeTask candidateProcessAccessTimeTask;
    @Resource
    private ConfigManager configManager;
    @Resource
    private ExamInfoCacheService examInfoCacheService;
    @Resource
    private IThirdInterfaceService iThirdInterfaceService;
    @Resource
    private LoginClient loginClient;
    @Resource
    private ILogService iLogService;
    @Resource
    private FaceVerifyConfig faceVerifyConfig;
    @Resource
    private IApplyCheckService applyCheckService;
    @Resource
    private IAnswerProgressService iAnswerProgressService;

    @Override
    public CandidateProcess getCandidateProcessByExamIdAndCandidateId(String examId, String candidateId) {
        return this.lambdaQuery()
                .eq(CandidateProcess::getExamId, examId)
                .eq(CandidateProcess::getCandidateId, candidateId)
                .eq(CandidateProcess::getDelFlag, false).last("LIMIT 1").one();
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public ApiResponse<CandidateInfoVO> doLogin(CandidateLoginRequestVO requestVO, HttpServletRequest request) {
        String examId = requestVO.getExamId();
        String admissionNumber = requestVO.getAdmissionNumber();
        String deviceType = requestVO.getDeviceType();
        String browserInfo = requestVO.getBrowserInfo();
        // 定义返回值对象
        CandidateInfoVO candidateInfoVO = CandidateInfoVO.builder().build();
        // 记录已登录过系统考生的的信息，为了不重复查询过程表数据，通过校验登录方法给该对象属性赋值
        CandidateProcess candidateProcess = new CandidateProcess();
        // 登录校验，顺便把admissionNumber参数值类型为身份证号是，转为对应的准考证号
        this.loginCheck(requestVO, candidateProcess, candidateInfoVO);
        boolean secondLogin = false;
        if (candidateInfoVO.getExamState() == 1) {
            // 允许登录
            candidateInfoVO.setLoginState(0);
            CandidateProcess entity = new CandidateProcess();
            entity.setDeviceType(deviceType);
            // 查询是否已经登录过
            if (StringUtils.isBlank(candidateProcess.getId())) {
                // 登录成功后考生过程表插入一条数据, 考生多处登录或并发情况下，可能会出现重复插入数据情况
                entity.setCandidateId(candidateInfoVO.getCandidateId());
                entity.setExamId(examId);
                entity.setAdmissionNumber(candidateInfoVO.getAdmissionNumber());
                entity.setExamStatus(CandidateStatusCode.LOGIN.getCode());
                entity.setLoginCount(0);
                //准考证加密
                String psn = String.valueOf(AdmissionNumberEncryptUtil.encrypt(candidateInfoVO.getAdmissionNumber(), candidateInfoVO.getCandidateId()));
                entity.setPasswordNumber(psn);
                candidateInfoVO.setPasswordNumber(entity.getPasswordNumber());
                try {
                    super.save(entity);
                } catch (DuplicateKeyException e) {
                    log.error("{}", e);
                }
            } else {
                if (candidateProcess.getExamStatus() > 0 || candidateProcess.getBeginTime() != null) {
                    secondLogin = true;
                }
                entity.setId(candidateProcess.getId());
                entity.setExamStatus(CandidateStatusCode.LOGIN.getCode());
                // 修改为已登录状态
                super.updateById(entity);
                //准考证加密
                candidateInfoVO.setPasswordNumber(candidateProcess.getPasswordNumber());
            }
            CandidateInfoUpdateDTO updateDTO = CandidateInfoUpdateDTO.builder().examId(examId)
                    .candidateId(candidateInfoVO.getCandidateId()).state(CandidateStatusCode.LOGIN.getCode())
                    .lateAllowLogin(false).build();

            boolean loginFlg = (candidateProcess.getLoginCount() != null && (candidateProcess.getLoginCount() > 1 || secondLogin));
            String ip = iLogService.getIp(request);
            if (!loginFlg) {
                //记录首次登录时间
                updateDTO.setLoginTime(LocalDateTime.now());
            }
            updateDTO.setLoginIp(ip);
            // 通知考务
            this.updateKwCandidateInfo(updateDTO);

            // 删除缓存信息
            this.deleteCache4MobileNotice(examId, candidateInfoVO.getCandidateId());
            // 保存考生浏览器信息
            this.saveCandidateBrowserInfo(examId, candidateInfoVO.getCandidateId(), admissionNumber, browserInfo);
            // 清空accessTime字段，为了剩余时长重新计算
            candidateProcessAccessTimeTask.setAccessTimeToNull(examId, candidateInfoVO.getCandidateId());

            iLogService.loginLog(examId, candidateInfoVO.getCandidateId(), loginFlg, requestVO.getFrom(), deviceType, ip);

        }
        return ApiResponse.<CandidateInfoVO>builder().code(BizStatusCode.KS_SUCCESS_OK.getCode()).data(candidateInfoVO).build();
    }

    public void loginCheck(CandidateLoginRequestVO requestVO, CandidateProcess candidateProcess, CandidateInfoVO candidateInfoVO) {
        String examId = requestVO.getExamId();
        String code = requestVO.getAdmissionNumber();
        String loginType = requestVO.getLoginType();

        if (StringUtils.isEmpty(examId) || StringUtils.isEmpty(code)) {
            throw new KsBizRunTimeException(BizStatusCode.KW_INVALID_PARAMETER.getCode(),
                    BizStatusCode.KW_INVALID_PARAMETER.getMsg());
        }
        // 如果考生输入参数为身份证，则需要转换下，把值转为准考证
        AdmissionVO xyAdmission = this.getAdmissionInfo(examId, code, loginType, requestVO.getFrom());
        if (xyAdmission.getCode() != BizStatusCode.KW_SUCCESS_OK.getCode().intValue()) {
            throw new KsBizRunTimeException(xyAdmission.getCode(), "");
        }
        String admissionNumber = xyAdmission.getAdmissionNumber();
        // 获取考试配置
        List<ExamConfigVO> configList = iExamCacheService.getExamConfigCache(examId);
        // 准考证号唯一，应该是只有一条数据而已
        CandidateProcess candidate = super.lambdaQuery().select(CandidateProcess::getExamStatus,
                        CandidateProcess::getCheckedIdCard, CandidateProcess::getBeginTime,
                        CandidateProcess::getLoginCount, CandidateProcess::getPasswordNumber, CandidateProcess::getId)
                .eq(CandidateProcess::getExamId, examId)
                .eq(CandidateProcess::getAdmissionNumber, admissionNumber)
                .eq(CandidateProcess::getDelFlag, false)
                .last("limit 1").one();
        if (candidate != null) {
            // 如果已经考试完成，则不允许登陆
            if (candidate.getExamStatus().intValue() >= CandidateStatusCode.COMMIT.getCode().intValue()) {
                throw new KsBizRunTimeException(BizStatusCode.HAVE_FINISH_EXAM.getCode());
            }
            candidateProcess.setLoginCount(candidate.getLoginCount());
            candidateProcess.setId(candidate.getId());
            candidateProcess.setPasswordNumber(candidate.getPasswordNumber());
            candidateProcess.setExamStatus(candidate.getExamStatus());
            candidateProcess.setBeginTime(candidate.getBeginTime());

            candidateInfoVO.setPasswordNumber(candidate.getPasswordNumber());
            //设置是否需要二次验证
            candidateInfoVO.setSecondLogin(Objects.nonNull(candidate.getCheckedIdCard()) ? candidate.getCheckedIdCard() : Boolean.FALSE);
            // 校验登录次数
            //this.loginLimit(candidate.getLoginCount(), configList);
        }
        // 老的校验方法，拆不动了
        this.check(examId, admissionNumber, candidateInfoVO, configList);
    }

    private void check(String examId, String admissionNumber, CandidateInfoVO candidateInfoVO,
                       List<ExamConfigVO> configList) {
        Integer examState = 0;
        // 提前登陆分钟数
        Integer earlyLogOnMinutes = 0;
        // 迟到分钟数后不允许登陆
        Integer delayLogOnMinutes = 0;

        Boolean videoMonitoring = false;
        // 获取考试信息
        ExamDTO examDTO = examInfoCacheService.getExamInfoFromCache(examId);
        if (Objects.nonNull(examDTO)) {
            Exam exam = examDTO.getExam();
            // 未发布考试，不允许登录
            if (!exam.getPublishExam()) {
                throw new KsBizRunTimeException(BizStatusCode.EXAM_NO_PUBLISH_FAIL.getCode(),
                        BizStatusCode.EXAM_NO_PUBLISH_FAIL.getMsg());
            }
            LocalDateTime now = LocalDateTime.now();
            Optional<ExamConfigVO> earlyLogOnConfig = configList.stream().filter(l -> l.getConfigCode()
                    .equals(ExamConfigCode.EXAM_CONFIG_EARLY_LOG_ON.getCode())).findFirst();
            Optional<ExamConfigVO> earlyLogOnMinsConfig = configList.stream().filter(l -> l.getConfigCode()
                    .equals(ExamConfigCode.EXAM_CONFIG_EARLY_LOG_ON_MINS.getCode())).findFirst();
            Optional<ExamConfigVO> delayLogOnConfig = configList.stream().filter(l -> l.getConfigCode()
                    .equals(ExamConfigCode.EXAM_CONFIG_DELAY_LOG_ON.getCode())).findFirst();
            Optional<ExamConfigVO> delayLogOnMinsConfig = configList.stream().filter(l -> l.getConfigCode()
                    .equals(ExamConfigCode.EXAM_CONFIG_DELAY_LOG_ON_MINS.getCode())).findFirst();
            Optional<ExamConfigVO> videoMonitoringConfig = configList.stream().filter(l -> l.getConfigCode()
                    .equals(ExamConfigCode.EXAM_CONFIG_VIDEO_MONITORING.getCode())).findFirst();

            Boolean earlyLogOn = earlyLogOnConfig.isPresent() ? earlyLogOnConfig.get().getConfigValue().equals("true") : false;
            earlyLogOnMinutes = earlyLogOnMinsConfig.isPresent() ? Integer.parseInt(earlyLogOnMinsConfig.get().getConfigValue()) : 0;
            Boolean delayLogOn = delayLogOnConfig.isPresent() ? delayLogOnConfig.get().getConfigValue().equals("true") : false;
            delayLogOnMinutes = delayLogOnMinsConfig.isPresent() ? Integer.parseInt(delayLogOnMinsConfig.get().getConfigValue()) : 0;
            videoMonitoring = videoMonitoringConfig.isPresent() ? videoMonitoringConfig.get().getConfigValue().equals("true") : false;

            // 已过考试时间
            if (exam.getEndDate().isBefore(now)) {
                examState = 2;
            } else if (!earlyLogOn && exam.getBeginDate().isAfter(now)) {
                // 没有设置提前开考时间，则直接判断开始时间是否到
                examState = 0;
            } else {
                examState = 1;
                if (earlyLogOn) {
                    // 考试开始时间+提前开考分钟数>北京时间，不允许登陆
                    if (exam.getBeginDate().minusMinutes(earlyLogOnMinutes).isAfter(now)) {
                        examState = 3;
                    }
                }
                if (examState == 1 && delayLogOn) {
                    String lateAllowLoginInfo = kwClient.getLateAllowLoginInfo(examId, admissionNumber);
                    ApiResponse<Boolean> lateAllowLoginInfoResponse = ApiResponse.ConvertRet(lateAllowLoginInfo, Boolean.class);
                    boolean flag = exam.getBeginDate().plusMinutes(delayLogOnMinutes).isAfter(now) ||
                            (lateAllowLoginInfoResponse.getData() != null && lateAllowLoginInfoResponse.getData());
                    if (!flag) {
                        examState = 4;
                    }
                }
            }
            candidateInfoVO.setDelayLogOnMins(delayLogOnMinutes);
            candidateInfoVO.setEarlyLogOnMins(earlyLogOnMinutes);
        }
        candidateInfoVO.setExamState(examState).setVideoMonitoring(videoMonitoring);
        if (examState == 1) {
            // 获取考生信息内容
            String resp = kwClient.getExamCandidateInfo(examId, admissionNumber);
            log.debug("调用考务系统获取考生信息：{}", resp);
            ApiResponse<ExamCandidate> clientResponse = ApiResponse.ConvertRet(resp, ExamCandidate.class);
            // 判断考务是否有考生
            if (clientResponse.getCode().intValue() != BizStatusCode.KW_SUCCESS_OK.getCode().intValue()) {
                throw new KsBizRunTimeException(clientResponse.getCode(), "");
            } else if (clientResponse.getData().getState() >= CandidateStatusCode.COMMIT.getCode().intValue()) {
                throw new KsBizRunTimeException(BizStatusCode.ABSENT_LOGIN_FAIL.getCode());
            }
            this.assembleData(clientResponse.getData(), candidateInfoVO);
        }
    }

    private void assembleData(ExamCandidate examCandidate, CandidateInfoVO candidateInfoVO) {
        candidateInfoVO.setAdmissionNumber(examCandidate.getAdmissionNumber());
        candidateInfoVO.setCandidateId(examCandidate.getId());
        candidateInfoVO.setExamId(examCandidate.getExamId());
        candidateInfoVO.setForbidEndExam(examCandidate.getForbidEndExam());
        candidateInfoVO.setState(examCandidate.getState());
    }

    private void saveCandidateBrowserInfo(String examId, String candidateId, String admissionNumber, String browserInfo) {
        CandidateBrowserInfo candidateBrowserInfo = CandidateBrowserInfo.builder().examId(examId)
                .candidateId(candidateId).admissionNumber(admissionNumber).browserInfo(browserInfo).build();
        candidateBrowserInfo.setId(UUIDUtils.randomUUID());
        candidateBrowserInfo.setCreateDate(LocalDateTime.now());
        candidateBrowserInfoMapper.insert(candidateBrowserInfo);
    }

    private void deleteCache4MobileNotice(String examId, String candidateId) {
        redisTemplate.delete(KeyDefineCommon.getMobileDeviceOffLineKey(examId, candidateId));
        redisTemplate.delete(KeyDefineCommon.getMobileDeviceMuteKey(examId, candidateId));
        redisTemplate.delete(KeyDefineCommon.getMobileDeviceMuteNoticeKey(examId, candidateId));
        redisTemplate.delete(KeyDefineCommon.getMobileDeviceOffLineNoticeKey(examId, candidateId));
    }

    @Override
    public ExamPaperTopicVO getTopic(String examId, String candidateId, HttpServletRequest request) {
        CandidateProcess candidateProcess = super.lambdaQuery().select(CandidateProcess::getExamStatus,
                        CandidateProcess::getId, CandidateProcess::getStartedExam)
                .eq(CandidateProcess::getExamId, examId).eq(CandidateProcess::getCandidateId, candidateId)
                .eq(CandidateProcess::getDelFlag, false).one();
        // 已交卷不，直接返回
        if (candidateProcess.getExamStatus().intValue() >= CandidateStatusCode.COMMIT.getCode().intValue()) {
            throw new KsBizRunTimeException(BizStatusCode.TEST_HAVE_FINISHED.getCode());
        }
        //获取考生试卷Id和试卷版本号，从考务或者缓存获取考生绑定的试卷信息
        CandidatePaperInfo candidatePaperInfo = iExamCacheService.getCandidatePaperInfoCache(examId, candidateId);
        if (candidatePaperInfo == null) {
            throw new KsBizRunTimeException(BizStatusCode.GET_PAPER_ERROR.getCode(), "获取失败试卷信息");
        }
        // 查询考生当前考到哪一个单元，为了命中索引，加了>mixSort条件查询
        List<CandidatePaper> candidatePaperList = iCandidatePaperService.lambdaQuery()
                .select(CandidatePaper::getPaperId, CandidatePaper::getPaperVersion, CandidatePaper::getId,
                        CandidatePaper::getHandInTime, CandidatePaper::getMixSort, CandidatePaper::getPaperType)
                .eq(CandidatePaper::getExamId, examId).gt(CandidatePaper::getMixSort, 0)
                .eq(CandidatePaper::getCandidateId, candidateId).eq(CandidatePaper::getFinish, false)
                .orderByAsc(CandidatePaper::getMixSort).list();
        //组装考生项目信息
        ExamInfoVO examInfo = ExamInfoVO.builder().candidateName(candidatePaperInfo.getCandidateName())
                .candidateId(candidateId).admissionNumber(candidatePaperInfo.getAdmissionNumber())
                .examName(candidatePaperInfo.getExamName())
                .randomId(candidatePaperInfo.getRandomId()).build();

        ExamPaperTopicVO vo = ExamPaperTopicVO.builder().examInfo(examInfo).build();
        if (candidatePaperList.size() == 0) {
            vo.setTotalPaper(0).setCurrentPaper(0);
        } else {
            CandidatePaper candidatePaper = candidatePaperList.get(0);
            String paperId = candidatePaper.getPaperId();
            String paperVersion = candidatePaper.getPaperVersion();
            // 查询所有大题
            List<PaperTopic> topicList = paperTopicMapper.selectList(Wrappers.<PaperTopic>lambdaQuery()
                    .select(PaperTopic::getTopicId, PaperTopic::getAnswer, PaperTopic::getSort, PaperTopic::getAnswer)
                    .eq(PaperTopic::getCandidatePaperId, candidatePaper.getId())
                    .orderByAsc(PaperTopic::getSort));
            //用与记录当前已经做到第几号大题
            examInfo.setHandInTime(candidatePaper.getHandInTime().toString());

            List<PaperTopicListVO> paperTopicListVOs = new ArrayList<>();

            int totalPaper = candidatePaperInfo.getPaperList().size();
            vo.setPaperSort(candidatePaper.getMixSort())
                    .setTotalPaper(totalPaper).setCurrentPaper(totalPaper - candidatePaperList.size() + 1)
                    .setPaperTopic(paperTopicListVOs).setPaperType(candidatePaper.getPaperType());
            //获取考生答案信息
            List<CandidateAnswer> candidateAnswers = iCandidateAnswerService.lambdaQuery()
                    .select(CandidateAnswer::getItemId, CandidateAnswer::getItemType, CandidateAnswer::getObjectiveOrSubjective,
                            CandidateAnswer::getItemDifficultyDegree, CandidateAnswer::getObjectiveAnswer, CandidateAnswer::getForceAnswer,
                            CandidateAnswer::getSubjectiveAnswer, CandidateAnswer::getFlagState, CandidateAnswer::getOptionMixSortRule,
                            CandidateAnswer::getItemMixSort, CandidateAnswer::getViewAnswerDescription)
                    .eq(CandidateAnswer::getExamId, examId)
                    .eq(CandidateAnswer::getCandidateId, candidateId).eq(CandidateAnswer::getPaperId, paperId)
                    .eq(CandidateAnswer::getPaperVersion, paperVersion)
                    .eq(CandidateAnswer::getDelFlag, false).list();
            // 按小题代码转成map，方便查询
            Map<String, CandidateAnswer> candidateAnswerMap = candidateAnswers.stream().collect(
                    Collectors.toMap(CandidateAnswer::getItemId, Function.identity(), (k1, k2) -> k1));
            //生成考生试卷页面左侧题号结构数据，循环大题
            for (PaperTopic paperTopic : topicList) {
                PTopicDTO topicDTO = iPaperService.getTopic(paperId, paperVersion, paperTopic.getTopicId());
                if (topicDTO == null) {
                    log.error("题库或者缓存都没有该试卷: {},{}", paperId, paperVersion);
                    throw new KsBizRunTimeException(BizStatusCode.GET_PAPER_ERROR.getCode(), "题库没有该试卷，请检查");
                }
                // 大题数据
                PaperTopicListVO paperTopicListVO = new PaperTopicListVO();
                paperTopicListVO.setTopicId(topicDTO.getTopicId());
                paperTopicListVO.setTopicName(topicDTO.getTopicName());
                paperTopicListVO.setTopicSort(topicDTO.getTopicSort());
                // 小题数据
                List<PaperItemVO> itemDataList = new ArrayList<PaperItemVO>();
                Map<String, PItemDTO> itemMap = iPaperService.getPaperItemByTopicId(paperId, paperVersion, topicDTO.getTopicId());
                itemMap.forEach((k, v) -> {
                    this.buildItemData4New(candidateAnswerMap, v, itemDataList);
                });
                paperTopicListVO.setItemDataList(itemDataList.stream()
                        .sorted(Comparator.comparing(PaperItemVO::getItemSort))
                        .collect(Collectors.toList()));
                //对有子小题的进行排序
                paperTopicListVO.getItemDataList().forEach(item -> {
                    List<PaperItemVO> childAnswer = item.getChildAnswer();
                    if (!CollectionUtils.isEmpty(childAnswer)) {
                        item.setChildAnswer(childAnswer.stream()
                                .sorted(Comparator.comparing(PaperItemVO::getItemSort))
                                .collect(Collectors.toList()));
                    }
                });
                paperTopicListVOs.add(paperTopicListVO);
            }
            // 性格测试卷
            if (PaperTypeCode.CHARACTER.getCode().intValue() == candidatePaper.getPaperType().intValue()) {
                // 性格卷下一页，依赖上面sql，注意上面sql一定要排序按照升序查询
                Optional<PaperTopic> nextPage = topicList.stream().filter(item -> item.getAnswer() == false).findFirst();
                if (nextPage.isPresent()) {
                    // 大题不乱序，所以这个大题顺序号与试卷一致
                    vo.setCurrent(nextPage.get().getSort());
                } else {
                    // 已经做完分页
                    vo.setCurrent(topicList.size());
                }
                vo.setTotal(topicList.size());
                //是否有作答
                Optional<CandidateAnswer> answer = candidateAnswers.stream()
                        .filter(item -> StringUtils.isNotBlank(item.getForceAnswer()) || StringUtils.isNotBlank(item.getObjectiveAnswer()))
                        .findFirst();
                vo.setAnswered(answer.isPresent());
                // 引导语类型
                vo.setGuideType(candidateAnswers.size() == configManager.getGuideTypeSchool().intValue() ? 0 : 1);
            }
        }
        // 更新开考状态
        this.updateCandidateProcess(candidateProcess);

        return vo;
    }

    /**
     * 更新考中状态和进入过作答界面状态
     *
     * @param candidateProcess
     */
    private void updateCandidateProcess(CandidateProcess candidateProcess) {
        // 更新考生开考状态（表示考生进入过作答界面看过试卷了）
        if (!candidateProcess.getStartedExam()) {
            this.lambdaUpdate().set(CandidateProcess::getStartedExam, true)
                    .eq(CandidateProcess::getId, candidateProcess.getId()).update();
        }
    }

    private void buildItemData4New(Map<String, CandidateAnswer> candidateAnswerMap, PItemDTO itemDataVO,
                                   List<PaperItemVO> itemDataList) {
        PaperItemVO paperItemVO = new PaperItemVO();
        CandidateAnswer candidateAnswer = candidateAnswerMap.get(itemDataVO.getItemId());
        // 1：未标记，2：已标记
        int isMark = 1;
        if (candidateAnswer != null && candidateAnswer.getFlagState() == 2) {
            isMark = 2;
        }
        // 0：未作答，1：已作答
        int isAnswer = 0;
        // 考生作答的答案
        List<AnswerVO> answer = new ArrayList<AnswerVO>();
        paperItemVO.setAttrsDataList(itemDataVO.getAttrsDataList());
        if (PaperItemTypeCode.ITEM_TYPE_FILL_IN_BLANKS_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType())) {
            itemDataVO.getOptionMap().forEach((k, v) -> {
                CandidateAnswer candidateAnswer2 = candidateAnswerMap.get(v.getOptionId());
                if (!candidateAnswer2.getObjectiveOrSubjective() && StringUtils.isNotBlank(candidateAnswer2.getSubjectiveAnswer())) {
                    answer.add(AnswerVO.builder().optionId(candidateAnswer2.getItemId())
                            .userAnswer(candidateAnswer2.getSubjectiveAnswer()).build());
                } else if (candidateAnswer2.getObjectiveOrSubjective() && StringUtils.isNotBlank(candidateAnswer2.getSubjectiveAnswer())) {
                    answer.add(AnswerVO.builder().optionId(candidateAnswer2.getItemId())
                            .userAnswer(candidateAnswer2.getObjectiveAnswer()).build());
                } else {
                    answer.add(null);
                }
            });
            if (answer.size() > 0 && answer.get(0) != null) {
                isAnswer = 1;
            }
        } else if (PaperItemTypeCode.ITEM_TYPE_JUDGMENT_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType())) {
            if (StringUtils.isNotBlank(candidateAnswer.getObjectiveAnswer())) {
                answer.add(AnswerVO.builder().optionId(candidateAnswer.getObjectiveAnswer())
                        .userAnswer(candidateAnswer.getObjectiveAnswer()).build());
                isAnswer = 1;
            }
        } else if (PaperItemTypeCode.ITEM_TYPE_MULTIPLE_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType())
                || PaperItemTypeCode.ITEM_TYPE_SINGLE_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType())
                || PaperItemTypeCode.ITEM_TYPE_WEIGHT_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType())
                || PaperItemTypeCode.ITEM_TYPE_INDEFINITE_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType())
                || PaperItemTypeCode.ITEM_TYPE_CLOZE_FILL_IN_BLANKS_CHILD_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType())) {
            Map<String, String> optionMap = this.changeKey(itemDataVO.getOptionMap());
            String answerString = candidateAnswer.getObjectiveAnswer();
            if (StringUtils.isNotBlank(answerString)) {
                String[] answerArray = answerString.split(",");
                for (String s : answerArray) {
                    answer.add(AnswerVO.builder().optionId(optionMap.get(s)).userAnswer(s).build());
                }
                if (PaperItemTypeCode.ITEM_TYPE_MULTIPLE_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType()) && answerArray.length < 2) {
                    isAnswer = 0;
                } else {
                    isAnswer = 1;
                }
            }
        } else if (PaperItemTypeCode.ITEM_TYPE_FORCE_CHOICE_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType())) {
            Map<String, String> optionMap = this.changeKey(itemDataVO.getOptionMap());
            String answerString = candidateAnswer.getObjectiveAnswer();
            if (StringUtils.isNotBlank(answerString)) {
                answer.add(AnswerVO.builder().optionId(optionMap.get(answerString)).userAnswer(answerString).build());
                isAnswer = 1;
            }
        } else if (PaperItemTypeCode.ITEM_TYPE_COMPOUND_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType())
                || PaperItemTypeCode.ITEM_TYPE_CASE_ANALYSIS_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType())
                || PaperItemTypeCode.ITEM_TYPE_CLOZE_FILL_IN_BLANKS_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType())
                || PaperItemTypeCode.ITEM_TYPE_MATCHING_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType())) {
            List<PaperItemVO> childItemDataList = new ArrayList<>();
            boolean flag = true;
            Collection<PItemDTO> pItemDTOs = itemDataVO.getChildItemMap().values();
            //设置大题下的小题数量，只用与限制考生回退时使用
            paperItemVO.setChildItemSize(pItemDTOs.size());
            for (PItemDTO pItemDTO : pItemDTOs) {
                CandidateAnswer as = candidateAnswerMap.get(pItemDTO.getItemId());
                if (StringUtils.isBlank(as.getObjectiveAnswer()) && StringUtils.isBlank(as.getSubjectiveAnswer())) {
                    flag = false;
                }
                this.buildItemData4New(candidateAnswerMap, pItemDTO, childItemDataList);
            }
            // 复合题下必须所有小题都作答，该复合题才能算已作答
            if (flag) {
                isAnswer = 1;
            }
            paperItemVO.setChildAnswer(childItemDataList);
        } else if (PaperItemTypeCode.ITEM_TYPE_PICTURE_ANSWER_CODE.getCode().equalsIgnoreCase(itemDataVO.getItemType())) {
            if (StringUtils.isNotBlank(candidateAnswer.getSubjectiveAnswer())) {
                String[] answerArray = candidateAnswer.getSubjectiveAnswer().split(",");
                for (String s : answerArray) {
                    answer.add(AnswerVO.builder().optionId(itemDataVO.getItemId()).userAnswer(s).build());
                }
                isAnswer = 1;
            }
        } else {
            if (candidateAnswer.getObjectiveOrSubjective()) {
                if (StringUtils.isNotBlank(candidateAnswer.getObjectiveAnswer())) {
                    answer.add(AnswerVO.builder().optionId(itemDataVO.getItemId()).userAnswer(candidateAnswer.getObjectiveAnswer()).build());
                    isAnswer = 1;
                }
            } else {
                if (StringUtils.isNotBlank(candidateAnswer.getSubjectiveAnswer())) {
                    answer.add(AnswerVO.builder().optionId(itemDataVO.getItemId()).userAnswer(
                            candidateAnswer.getSubjectiveAnswer()).build());
                    isAnswer = 1;
                }
            }
        }
        // 小题信息
        paperItemVO.setItemId(itemDataVO.getItemId());
        paperItemVO.setItemSort(candidateAnswer.getItemMixSort());
        paperItemVO.setIsAnswer(isAnswer);
        paperItemVO.setIsMark(isMark);
        paperItemVO.setItemType(itemDataVO.getItemType());
        paperItemVO.setViewAnswerDescription(candidateAnswer.getViewAnswerDescription());
        paperItemVO.setUserAnswer(answer);
        itemDataList.add(paperItemVO);
    }

    private Map<String, String> changeKey(Map<String, POptionDTO> optionDTOMap) {
        Map<String, String> optionMap = new HashMap<>();
        optionDTOMap.forEach((k, v) -> {
            optionMap.put(v.getOptionName(), v.getOptionId());
        });
        return optionMap;
    }

    /**
     * 获取倒计时时长-一般考试
     *
     * @param examId
     * @param candidateId
     * @return
     */
    @SneakyThrows
    @Override
    public CandidateStatusVO getTimeLength(String examId, String candidateId, Integer sort) {
        CandidateTimeLength candidateTimeLength = iCandidatePaperService.getTimeLength(examId, candidateId, sort);
        //当前时间
        long currentTimeMillis = System.currentTimeMillis();
        CandidateStatusVO vo = CandidateStatusVO.builder().build();
        if (candidateTimeLength != null && candidateTimeLength.getExamStatus() != null &&
                candidateTimeLength.getExamStatus().intValue() < CandidateStatusCode.COMMIT.getCode().intValue()) {

            CandidatePaper candidatePaper = new CandidatePaper();
            candidatePaper.setId(candidateTimeLength.getCandidatePaperId());
            boolean noticeKw = false;
            // 用于通知考务系统
            CandidateInfoUpdateDTO candidateInfoUpdateVO = CandidateInfoUpdateDTO.builder().examId(examId)
                    .candidateId(candidateId).build();
            // 考务已经强制结束考试
            if (candidateTimeLength.getAccessTime() == null) {
                //首次访问 AccessTime可能为空
                candidatePaper.setAccessTime(currentTimeMillis);
                candidateTimeLength.setAccessTime(currentTimeMillis);
            }
            if (candidateTimeLength.getBeginTime() == null) {
                // 开考时间只设置一次
                LocalDateTime startDate = LocalDateTime.now();
                candidateInfoUpdateVO.setExamBeginDate(startDate);
                baseMapper.updateBeginTimeById(candidateTimeLength.getId(), startDate);
                noticeKw = true;
            }
            // 非重新登录，直接计算剩余时长
            long remainTime = this.calcRemainTime(candidateTimeLength, currentTimeMillis, sort);

            long useTime = candidateTimeLength.getPaperTimeLength() - remainTime;/* + candidateTimeLength.getExtraTime().longValue() */
            candidatePaper.setExamUseTime(useTime);
            log.debug("# 获取剩余时长接口计算ExamUseTime：{}", candidatePaper.getExamUseTime());
            candidatePaper.setRemainingTime(remainTime);
            candidatePaper.setAccessTime(currentTimeMillis);
            // 实时更新剩余时长
            iCandidatePaperService.updateById(candidatePaper);
            // 更新考试用时时间
            this.updateUserTimeKw(examId, candidateId);
            // 提前交卷
            boolean flag = candidateTimeLength.getPaperTimeLength() -
                    remainTime >= candidateTimeLength.getHandInTime() * 60 * 1000;
            long timeLength = remainTime + candidateTimeLength.getExtraTime();
            if (timeLength < 0) {
                timeLength = 0;
            }
            // 试卷总时 长减去剩余时长=已考时长
            vo.setTimeLength(timeLength).setAllowFinishExam(flag)
                    .setForbidFinish(candidateTimeLength.getForbidFinishExam());
            if (noticeKw) {
                this.updateKwCandidateInfo(candidateInfoUpdateVO);
            }
        } else if (candidateTimeLength.getForbidFinishExam()) {
            log.info("倒计时强制结束考试", JSON.toJSONString(candidateTimeLength));
            long remainTime = this.calcRemainTime(candidateTimeLength, currentTimeMillis, sort);
            long timeLength = remainTime + candidateTimeLength.getExtraTime();
            if (timeLength < 0) {
                timeLength = 0;
            }
            // 强制交卷
            vo.setTimeLength(timeLength).setAllowFinishExam(true).setForbidFinish(true);
        } else {
            log.info("倒计时考试已结束", JSON.toJSONString(candidateTimeLength));
            // 异常情况下进入考试，直接结束
            vo.setTimeLength(Constants.ZERO).setAllowFinishExam(true).setForbidFinish(false);
        }
        // 设置移动端提醒通知
        this.setAppNotice(examId, candidateId, vo);
        return vo;
    }

    private void updateUserTimeKw(String examId, String candidateId) {
        List<CandidateUseTimeDTO> candidateUseTimeList = iCandidatePaperService.getCandidateUseTime(examId, candidateId);
        Long examUseTime = candidateUseTimeList.stream().collect(Collectors.summingLong(CandidateUseTimeDTO::getUseTime));
        if (examUseTime != null && examUseTime > 0) {
            kwClient.updateCandidateUseTime(examId, candidateId, examUseTime);
        }
    }

    /**
     * @param candidateTimeLength
     * @param currentTimeMillis
     * @return （单位毫秒）
     */
    @Override
    public Long calcRemainTime(CandidateTimeLength candidateTimeLength, Long currentTimeMillis, Integer paperSort) {
        return this.calcRemainTime(candidateTimeLength.getExamId(), candidateTimeLength.getCandidateId(), candidateTimeLength.getExtraTime(),
                candidateTimeLength.getRemainingTime(), candidateTimeLength.getAccessTime(), currentTimeMillis, paperSort);
    }

    /**
     * 返回毫秒
     *
     * @param examId
     * @param extraTime
     * @param remainingTime
     * @param accessTime
     * @param currentTimeMillis
     * @return
     */
    @Override
    public Long calcRemainTime(String examId, String candidateId, Long extraTime, Long remainingTime, Long accessTime,
                               Long currentTimeMillis, Integer paperSort) {
        if (iExamCacheService.isUnifiedStartEnd(examId)) {
            return this.calcRemainTimeByUnifiedStartEnd(examId, candidateId, currentTimeMillis, extraTime, paperSort);
        } else {
            return this.calcRemainTimeByNormal(remainingTime, accessTime, currentTimeMillis, extraTime);
        }
    }

    /**
     * 返回分钟
     *
     * @param candidatePaper
     * @return
     */
    @Override
    public Long getRemainTime(CandidatePaper candidatePaper) {
        long timeLength = candidatePaper.getRemainingTime() + candidatePaper.getExtraTime();
        if (timeLength < 0) {
            timeLength = 0L;
        } else {
            timeLength = timeLength / 60 / 1000;
        }
        return timeLength;
    }

    /**
     * 正常考试倒计时计算方式（单位毫秒）
     *
     * @param remainTime
     * @param accessTime
     * @param currentTimeMillis
     * @return
     */
    @Override
    public Long calcRemainTimeByNormal(long remainTime, long accessTime, Long currentTimeMillis, long extraTime) {
        // 距离上次请求的时间差
        long diffTime = currentTimeMillis - accessTime;
        // 剩余时长=原剩余时长-时间差
        remainTime = remainTime - diffTime;
        if (extraTime == 0) {
            if (remainTime < Constants.ZERO) {
                remainTime = Constants.ZERO;
            }
        }
        return remainTime;
    }

    /**
     * 统一开考同一结束考试，倒计时方式（单位毫秒）
     *
     * @param examId
     * @param currentTimeMillis
     * @return
     */
    private Long calcRemainTimeByUnifiedStartEnd(String examId, String candidateId, Long currentTimeMillis, Long extraTime, Integer paperSort) {
        List<CandidatePaper> candidatePaperList = this.getCandidatePaperFromCache(examId, candidateId);
        candidatePaperList = candidatePaperList.stream().filter(c -> c.getSort().intValue() > paperSort.intValue()).collect(Collectors.toList());
        Long remainPaperTime = 0L;
        if (candidatePaperList.size() > 0) {
            remainPaperTime = candidatePaperList.stream().map(c -> c.getPaperTimeLength()).reduce(0L, Long::sum);
        }
        //试卷结束时间-当前时间
        long paperEndDateMilli = iExamCacheService.getPaperEndDateMilli(examId);
        long remainTime = paperEndDateMilli - currentTimeMillis - remainPaperTime;
        log.info("统一开考同一结束考试calcRemainTimeByUnifiedStartEnd倒计时计算{}、{}、{}、{}", remainPaperTime, paperEndDateMilli, currentTimeMillis, JSON.toJSONString(candidatePaperList));
        if (extraTime == 0) {
            if (remainTime < Constants.ZERO) {
                remainTime = Constants.ZERO;
            }
        }
        return remainTime;
    }

    private List<CandidatePaper> getCandidatePaperFromCache(String examId, String candidateId) {
        String key = KeyDefineCommon.getCandidatePaperListKey(examId, candidateId);
        Object val = redisTemplate.opsForValue().get(key);
        if (val == null) {
            List<CandidatePaper> candidatePaperList = iCandidatePaperService.list(Wrappers.<CandidatePaper>lambdaQuery()
                    .select(CandidatePaper::getPaperTimeLength, CandidatePaper::getSort)
                    .eq(CandidatePaper::getExamId, examId).eq(CandidatePaper::getCandidateId, candidateId));
            redisTemplate.opsForValue().set(key, JSON.toJSONString(candidatePaperList), 6, TimeUnit.HOURS);
            return candidatePaperList;
        } else {
            return JSON.parseArray(val.toString(), CandidatePaper.class);
        }
    }

    @Override
    public void updateKwCandidateInfo(CandidateInfoUpdateDTO obj) {
        try {
            // 通知考务更新考生状态，成功与否不能影响本功能实现
            candidateInfoProducer.sendMessage(obj);
        } catch (Exception e) {
            log.error("通知考务更新考生状态失败: ", e);
        }
    }

    /**
     * 获取小题数据
     *
     * @param itemId      小题id
     * @param candidateId 考生id
     * @param examId
     * @return
     */
    @Override
    public PaperBigQuestionVO getQuestion(String itemId, String candidateId, String examId, Integer paperSort,
                                          String language, Integer childItemSort, HttpServletRequest request) {
        PaperBigQuestionVO paperTopic = iPaperService.getItem(candidateId, examId, itemId, paperSort, language, childItemSort);
        this.setExamStatus(examId, candidateId, paperSort, paperTopic);
        return paperTopic;
    }

    private void setExamStatus(String examId, String candidateId, Integer paperSort, PaperBigQuestionVO paperTopic) {
        //考试状态，如果考试结束，则提示考生交卷
        QueryWrapper<CandidateProcess> wrapper = new QueryWrapper<CandidateProcess>();
        wrapper.lambda().eq(CandidateProcess::getCandidateId, candidateId).eq(CandidateProcess::getExamId, examId);
        CandidateTimeLength candidateTimeLength = iCandidatePaperService.getTimeLength(examId, candidateId, paperSort);
        int examStatus = candidateTimeLength.getExamStatus().intValue();
        if (candidateTimeLength.getForbidFinishExam() != null && candidateTimeLength.getForbidFinishExam() &&
                (examStatus == CandidateStatusCode.COMMIT.getCode().intValue() ||
                        examStatus == CandidateStatusCode.NOT_COMPLETE_BY_REASON.getCode().intValue())) {
            // 强制收卷
            paperTopic.setExamStatus(2);
        } else if ((candidateTimeLength.getRemainingTime() + candidateTimeLength.getExtraTime()) <= 0) {
            //倒计时为0
            paperTopic.setExamStatus(1);
        } else {
            paperTopic.setExamStatus(0);
        }
    }

    private CandidateAnswer getCandidateAnswer(String examId, String candidateId, String paperId, String paperVersion,
                                               String itemId) {
        LambdaQueryWrapper<CandidateAnswer> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.select(CandidateAnswer::getItemMixSort, CandidateAnswer::getOptionMixSortRule)
                .eq(CandidateAnswer::getExamId, examId).eq(CandidateAnswer::getCandidateId, candidateId)
                .eq(CandidateAnswer::getItemId, itemId).eq(CandidateAnswer::getPaperId, paperId)
                .eq(CandidateAnswer::getPaperVersion, paperVersion).eq(CandidateAnswer::getDelFlag, false);
        return candidateAnswerMapper.selectOne(queryWrapper);
    }

    @Override
    public void updateCandidateOnlineExamState(String examId, String examCandidateId, Boolean onlineExamState, Integer state) {
        CandidateProcess cp = super.lambdaQuery().select(CandidateProcess::getId, CandidateProcess::getExamStatus)
                .eq(CandidateProcess::getExamId, examId).eq(CandidateProcess::getCandidateId, examCandidateId).one();
        if (cp.getExamStatus() < CandidateStatusCode.COMMIT.getCode()) {
            boolean succ = false;
            if (onlineExamState) {
                // 变为考中，作答界面发消息到后端
                succ = super.lambdaUpdate().eq(CandidateProcess::getId, cp.getId())
                        .and(i -> i.eq(CandidateProcess::getExamStatus, CandidateStatusCode.LOGIN.getCode())
                                .or().eq(CandidateProcess::getExamStatus, CandidateStatusCode.INTERRUPT.getCode()))
                        .set(CandidateProcess::getExamStatus, CandidateStatusCode.TESTING.getCode()).update();
            } else {
                // 变为中断
                succ = super.lambdaUpdate().eq(CandidateProcess::getId, cp.getId())
                        .eq(CandidateProcess::getExamStatus, CandidateStatusCode.TESTING.getCode())
                        .set(CandidateProcess::getExamStatus, CandidateStatusCode.INTERRUPT.getCode()).update();
            }
            if (succ) {
                kwClient.updateCandidateOnlineExamState(examId, examCandidateId, onlineExamState, state);
            }
        }
    }

    @Override
    public void cancelSubmitPaper(CancelSubmitPaperDTO dto) {
        String examId = dto.getExamId();
        for (String s : dto.getExamCandidateIds()) {
            // 先查询数据，已交卷状态才允许撤销
            CandidateProcess candidateProcess = this.lambdaQuery()
                    .select(CandidateProcess::getPaperMode)
                    .eq(CandidateProcess::getExamId, examId).eq(CandidateProcess::getCandidateId, s)
                    .ge(CandidateProcess::getExamStatus, CandidateStatusCode.TESTING.getCode()).one();
            if (PaperModeCode.SINGLE_PAPER.getCode().equalsIgnoreCase(candidateProcess.getPaperMode())) {
                CandidatePaper candidatePaper = iCandidatePaperService.getOne(Wrappers.<CandidatePaper>lambdaQuery()
                        .eq(CandidatePaper::getExamId, examId).eq(CandidatePaper::getCandidateId, s));

                iCandidatePaperService.lambdaUpdate().set(CandidatePaper::getFinish, false)
                        .eq(CandidatePaper::getId, candidatePaper.getId()).update();
                //清理一下计时的进度
                iAnswerProgressService.resetAnswerTimeCancelPaper(examId,s,candidatePaper.getId());
                iLogService.recoverExamLog(examId, s, dto.getUserId(), candidatePaper.getPaperId(), candidatePaper.getPaperVersion());
            }

            // 考试过程表部分字段数据，恢复到中断状态，试卷包或单体试卷都需要执行该方法
            baseMapper.cancelSubmitPaper(examId, s);
            //如果考生的答题已经备份放回原表
            if (Objects.equals(1, dto.getIsBackupMap().get(s))) {
                iCandidateAnswerBackupService.cancelPackageCommit(examId, s, dto.getLogBackupMap().get(s));
            }
        }
    }

    /**
     * 重置考试
     *
     * @param examId
     * @param candidateId
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void redoExam(String examId, String candidateId) {
        //查询过程表数据
        QueryWrapper<CandidateProcess> wrapper = new QueryWrapper<CandidateProcess>();
        wrapper.lambda().eq(CandidateProcess::getCandidateId, candidateId)
                .eq(CandidateProcess::getExamId, examId);
        CandidateProcess cp = this.getOne(wrapper);
        if (cp == null || cp.getExamStatus() < CandidateStatusCode.COMMIT.getCode()) {
            throw new KsBizRunTimeException(BizStatusCode.REDO_EXAM_STATUS_FAIL.getCode());
        }
        //2.重置过程表
        redoCandidateProcess(examId, candidateId, cp);
        //3.答案表复制原始表
        //3.1判断是否已移动至备份表
        String ecRsp = kwClient.getExamCandidateById(candidateId);
        log.info("调用考务系统获取考生信息，：{}", ecRsp);
        ApiResponse<ExamCandidate> ecResponse = ApiResponse.ConvertRet(ecRsp, ExamCandidate.class);
        if (!Objects.equals(ecResponse.getCode(), BizStatusCode.KW_SUCCESS_OK.getCode()) || Objects.isNull(ecResponse.getData())) {
            log.error("调用考务系统获取考生信息失败，返回信息为:{}", ecResponse);
            throw new KsBizRunTimeException();
        }
        ExamCandidate ec = ecResponse.getData();
        if (Objects.equals(1, ec.getIsBackup())) {
            iCandidateAnswerBackupService.redoExam(examId, candidateId, cp.getRedoExamCount());
        } else {
            //initPaper(examId, candidateId,redoExamCount);
            redoCandidateAnswer(examId, candidateId, cp);
        }

        //1.重置考务状态栏值
        String repRtn = kwClient.examAgainClearCandidateData(examId, candidateId);
        ApiResponse res = JSON.parseObject(repRtn, ApiResponse.class);
        if (res.getCode().intValue() != BizStatusCode.KW_SUCCESS_OK.getCode().intValue()) {
            throw new KsBizRunTimeException(BizStatusCode.REDO_EXAM_KW_FAIL.getCode());
        }
    }

    /**
     * 重置过程数据
     *
     * @param examId
     * @param candidateId
     * @param cp
     */
    private void redoCandidateProcess(String examId, String candidateId, CandidateProcess cp) {
        LambdaQueryWrapper<CandidateProcess> queryWrapper = Wrappers.<CandidateProcess>lambdaQuery()
                .eq(CandidateProcess::getExamId, examId).eq(CandidateProcess::getCandidateId, candidateId)
                .eq(CandidateProcess::getDelFlag, false);
        CandidateProcess candidate = super.getOne(queryWrapper);
        LambdaUpdateWrapper<CandidateProcess> updateWrapper = Wrappers.<CandidateProcess>lambdaUpdate()
                .eq(CandidateProcess::getId, candidate.getId()).set(CandidateProcess::getForbidFinishExam, false)
                .set(CandidateProcess::getLoginCount, 0).set(CandidateProcess::getFinishType, null)
                .set(CandidateProcess::getBeginTime, null).set(CandidateProcess::getEndTime, null)
                .set(CandidateProcess::getChangScreenNumber, 0).set(CandidateProcess::getCheckedIdCard, false)
                .set(CandidateProcess::getTakePicture, false).set(CandidateProcess::getMobileLoginStatus, 0)
                .set(CandidateProcess::getRedoExamCount, cp.getRedoExamCount() + 1)
                .set(CandidateProcess::getStartedExam, false)
                .set(CandidateProcess::getExamStatus, CandidateStatusCode.UN_LOGIN.getCode());
        super.update(updateWrapper);
        iCandidatePaperService.updateTimeRef(examId, candidateId);
    }

    /**
     * 重置答案数据
     *
     * @param examId
     * @param candidateId
     * @param cp
     */
    private void redoCandidateAnswer(String examId, String candidateId, CandidateProcess cp) {
        //1.假删除当前考生的答案数据
        List<CandidateAnswer> list = iCandidateAnswerService.lambdaQuery().select(CandidateAnswer::getId, CandidateAnswer::getExamId,
                        CandidateAnswer::getCandidateId, CandidateAnswer::getAdmissionNumber, CandidateAnswer::getItemMixSort,
                        CandidateAnswer::getParentItemId, CandidateAnswer::getItemId, CandidateAnswer::getItemDifficultyDegree,
                        CandidateAnswer::getItemType, CandidateAnswer::getObjectiveOrSubjective, CandidateAnswer::getOptionMixSortRule,
                        CandidateAnswer::getStandardAnswer, CandidateAnswer::getFullScore, CandidateAnswer::getTopicId,
                        CandidateAnswer::getPaperId, CandidateAnswer::getPaperVersion, CandidateAnswer::getAnswerVer,
                        CandidateAnswer::getItemSerialNumber)
                .eq(CandidateAnswer::getExamId, examId)
                .eq(CandidateAnswer::getCandidateId, candidateId).eq(CandidateAnswer::getDelFlag, false).list();
        List<CandidateAnswer> updateList = new ArrayList<>();
        list.stream().forEach(item -> {
            CandidateAnswer entity = new CandidateAnswer();
            entity.setId(item.getId());
            entity.setDelFlag(true);
            // 记录老ID，更新为假删除状态
            updateList.add(entity);

            // 新的数据重置ID
            item.setId(UUIDUtils.randomUUID());
            item.setAnswerVer(item.getAnswerVer() + 1);
            item.setFlagState(1);
            item.setViewAnswerDescription(0);
        });
        iCandidateAnswerService.updateBatchById(updateList);
        iCandidateAnswerService.saveBatch(list);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void initCandidateData(InitCandidateDataVO vo) {
//        this.initProcessData(vo.getCandidateId(), vo.getExamId(), vo.getAdmissionNumber());
//        this.initCandidateAnswerData(vo.getExamId(), vo.getCandidateId(), vo.getAdmissionNumber(), vo.getPaperId(),
//                vo.getPaperVersion(), 0, 0);
    }

    private MobileDeviceNoticeDTO getMobileDeviceNotice(String examId) {
        List<ExamConfigVO> examConfigList = iExamCacheService.getExamConfigCache(examId);
        // 自定义移动信息提醒
        MobileDeviceNoticeDTO dto = MobileDeviceNoticeDTO.builder().build();

        examConfigList.parallelStream().forEach(item -> {
            if (item.getConfigCode().equalsIgnoreCase(ExamConfigCode.EXAM_CONFIG_CODE_APP_OFFLINE_REMIND.getCode())) {
                dto.setOfflineText(StringUtil.trims(item.getConfigValue()));
            }
            if (item.getConfigCode().equalsIgnoreCase(ExamConfigCode.EXAM_CONFIG_CODE_APP_MUTE_REMIND.getCode())) {
                dto.setMuteText(StringUtil.trims(item.getConfigValue()));
            }
            if (item.getConfigCode().equalsIgnoreCase(ExamConfigCode.EXAM_CONFIG_CODE_APP_MUTE_REMIND_MINS.getCode())) {
                dto.setMuteMinute(Integer.valueOf(item.getConfigValue()));
            }
            if (item.getConfigCode().equalsIgnoreCase(ExamConfigCode.EXAM_CONFIG_CODE_APP_OFFLINE_REMIND_MINS.getCode())) {
                dto.setOfflineMinute(Integer.valueOf(item.getConfigValue()));
            }
        });
        return dto;
    }

    /**
     * 设置app提醒信息
     *
     * @param examId
     * @param examCandidateId
     * @param vo
     */

    public void setAppNotice(String examId, String examCandidateId, CandidateStatusVO vo) {
        try {
            MobileDeviceNoticeDTO config = this.getMobileDeviceNotice(examId);
            StringBuffer sb = new StringBuffer();
            if (config != null) {
                // 获取当前时间（精确到秒）
                long nowSecond = DateUtil.getSecondByNow();
                String muteKey = KeyDefineCommon.getMobileDeviceMuteKey(examId, examCandidateId);
                String preMuteNoticeKey = KeyDefineCommon.getMobileDeviceMuteNoticeKey(examId, examCandidateId);
                String offLineKey = KeyDefineCommon.getMobileDeviceOffLineKey(examId, examCandidateId);
                String preOffLineNoticeKey = KeyDefineCommon.getMobileDeviceOffLineNoticeKey(examId, examCandidateId);
                // 考试配置开启离线提醒
                if (StringUtils.isNotBlank(config.getOfflineText())) {
                    Long offLineTime = null;
                    Object offLineVal = redisTemplate.opsForValue().get(offLineKey);
                    if (offLineVal != null) {
                        offLineTime = Long.valueOf(offLineVal.toString());
                    }
                    sb.append(",offLineKey:" + offLineTime);
                    Object val = redisTemplate.opsForValue().get(preOffLineNoticeKey);
                    // 上次通知时间
                    Long preOffLineNoticeTime = null;
                    if (val != null) {
                        preOffLineNoticeTime = Long.valueOf(val.toString());
                    }
                    sb.append(",preOffLineNoticeKey:" + preOffLineNoticeTime);
                    boolean flag = false;
                    if (preOffLineNoticeTime == null && offLineTime != null) {
                        // 第一次通知
                        if ((nowSecond - offLineTime.longValue()) >= config.getOfflineMinute() * 60) {
                            vo.setOfflineText(config.getOfflineText());
                            redisTemplate.delete(offLineKey);
                            // 记录本次时间，为下次通知做准备
                            this.setCacheForNotice(preOffLineNoticeKey);
                            // 把静音状态删除，要不然前端会一直交替提示，只要离线，就不用提示静音
                            flag = true;
                        }
                    } else if (preOffLineNoticeTime != null && offLineTime == null) {
                        // 第N次通知
                        if ((nowSecond - preOffLineNoticeTime.longValue()) >= config.getOfflineMinute() * 60) {
                            vo.setOfflineText(config.getOfflineText());
                            // 记录本次时间，为下次通知做准备
                            this.setCacheForNotice(preOffLineNoticeKey);
                            // 把静音状态删除，要不然前端会一直交替提示，只要离线，就不用提示静音
                            flag = true;
                        }
                    } else if (preOffLineNoticeTime != null && offLineTime != null) {
                        if (redisTemplate.hasKey(preOffLineNoticeKey)) {
                            redisTemplate.delete(preOffLineNoticeKey);
                        }
                    }
                    if (flag) {
                        // 把静音状态删除，要不然前端会一直交替提示，只要离线，就不用提示静音
                        if (redisTemplate.hasKey(muteKey)) {
                            redisTemplate.delete(muteKey);
                        }
                        if (redisTemplate.hasKey(preMuteNoticeKey)) {
                            redisTemplate.delete(preMuteNoticeKey);
                        }
                    }
                }
                // 只提醒一种类型
                if (vo.getOfflineText() == null) {
                    // 配置开启
                    if (StringUtils.isNotBlank(config.getMuteText())) {
                        Long muteTime = (Long) redisTemplate.opsForValue().get(muteKey);
                        sb.append(",muteKey:" + muteTime);
                        // 上次通知时间
                        Long preMuteNoticeTime = (Long) redisTemplate.opsForValue().get(preMuteNoticeKey);
                        sb.append(",preMuteNoticeKey:" + preMuteNoticeTime);
                        if (preMuteNoticeTime == null && muteTime != null) {
                            // 第一次通知
                            this.setCacheForNotice(preMuteNoticeKey);
                        } else if (preMuteNoticeTime != null && muteTime != null) {
                            // 第N次通知
                            if ((nowSecond - preMuteNoticeTime.longValue()) >= config.getMuteMinute() * 60) {
                                vo.setMuteText(config.getMuteText());
                                // 记录本次时间，为下次通知做准备
                                this.setCacheForNotice(preMuteNoticeKey);
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            log.error("app提醒信息异常：{}", e.getMessage());
        }
    }

    private void setCacheForNotice(String key) {
        redisTemplate.opsForValue().set(key, DateUtil.getSecondByNow(), 12, TimeUnit.HOURS);
    }

    @Override
    public AdmissionVO getAdmissionInfo(String examId, String admissionNumber, String loginType, String from) {
        AdmissionVO admissionInfo = new AdmissionVO();
        admissionInfo.setCode(BizStatusCode.KW_SUCCESS_OK.getCode().intValue());
        admissionInfo.setAdmissionNumber(admissionNumber);
        List<ExamConfigVO> examConfigList = iExamCacheService.getExamConfigCache(examId);
        if ("1".equals(from)) {
            Boolean idCardLogin = examConfigList.stream().filter(ecl ->
                    ExamConfigCode.EXAM_CONFIG_CODE_ID_CARD_LOGIN.getCode().equals(ecl.getConfigCode())
                            && "true".equals(ecl.getConfigValue())).count() > 0;
            Boolean phoneLogin = examConfigList.stream().filter(ecl ->
                    ExamConfigCode.EXAM_CONFIG_CODE_PHONE_NUM_LOGIN.getCode().equals(ecl.getConfigCode())
                            && "true".equals(ecl.getConfigValue())).count() > 0;
            if (("1".equals(loginType) && idCardLogin) || ("2".equals(loginType) && phoneLogin)) {
                String resAdmissionNums = "";
                if ("1".equals(loginType)) {
                    resAdmissionNums = kwClient.getAdmissionNumsByIdCardNum(examId, admissionNumber);
                } else {
                    resAdmissionNums = kwClient.getAdmissionNumsByPhone(examId, admissionNumber);
                }
                ApiResponse<JSONArray> resAdmissionNumsResponse = JSON.parseObject(resAdmissionNums, ApiResponse.class);
                if (resAdmissionNumsResponse.getCode().intValue() != BizStatusCode.KW_SUCCESS_OK.getCode().intValue()) {
                    admissionInfo.setCode(resAdmissionNumsResponse.getCode().intValue());
                    return admissionInfo;
                }
                List<String> admissionNums = JSONObject.parseArray(resAdmissionNumsResponse.getData().toJSONString(), String.class);
                // 身份证号登录，只能是考试下只有一身份证号的考生，不能存在多个考生同身份证号，如果是存在多个考生同身份证号，则必须准考证号登录
                if (admissionNums.size() == 1) {
                    admissionInfo.setAdmissionNumber(admissionNums.get(0));
                    return admissionInfo;
                } else if (admissionNums.size() > 1) {
                    throw new KsBizRunTimeException(BizStatusCode.EXIT_MANY_ID_CARD.getCode());
                } else {
                    if ("1".equals(loginType)) {
                        throw new KsBizRunTimeException(BizStatusCode.NOT_EXIST_ID_CARD_NUMBER.getCode());
                    } else {
                        throw new KsBizRunTimeException(BizStatusCode.NOT_EXIST_PHONE_NUMBER.getCode());
                    }
                }
            }
        }
        return admissionInfo;
    }

    /**
     * 性格测试卷翻页
     *
     * @param candidateId
     * @param examId
     * @param current
     * @param sort
     * @return
     */
    @Override
    public PaperTopicItemListVO getQuestionListByTopicId(String candidateId, String examId, Integer current, Integer sort) {
        PaperTopicItemListVO paperTopicItemListVO = new PaperTopicItemListVO();
        // 校验上一页是否做完，没做完不允许下一页，放到保存答案处理
        this.checkBeforePageIsFinish(examId, candidateId, sort, current);
        // 组装二次迫选题
        this.setItemList4New(candidateId, examId, current, sort, paperTopicItemListVO);
        return paperTopicItemListVO;
    }

    /**
     * 检查当前页是否做完所有小题，只用于性格测试卷检验
     *
     * @param candidateId
     * @param examId
     * @param page
     * @param paperSort
     */
    private void checkBeforePageIsFinish(String candidateId, String examId, Integer page, Integer paperSort) {
        if (page > 0) {
            // 获取未做完题目
            int c = candidateAnswerMapper.countNotFinishItemNumber(examId, candidateId, paperSort, page - 1);
            if (c > 0) {
                throw new KsBizRunTimeException(BizStatusCode.NOT_ANSWER_ALL_QUESTION_CURRENT_PAGE.getCode());
            }
        }
    }

    /**
     * 设置小题
     *
     * @param candidateId
     * @param examId
     * @param current
     * @param paperSort
     * @param paperTopic
     */
    private void setItemList4New(String candidateId, String examId, Integer current, int paperSort,
                                 PaperTopicItemListVO paperTopic) {
        CandidatePaper candidatePaper = iPaperService.getCandidatePaperFromCache(examId, candidateId, paperSort);
        // 获取大题ID
        String topicId = paperTopicMapper.getTopicId(candidatePaper.getId(), current);
        // 查询中断情况下，性格测试卷是否停留在二次迫选的页面未提交答案，未保存有客观题答案字段，可以视为未进行二次迫选
        List<CandidateAnswer> candidateAnswers = iCandidateAnswerService.list(Wrappers.<CandidateAnswer>lambdaQuery()
                .select(CandidateAnswer::getId, CandidateAnswer::getItemId, CandidateAnswer::getObjectiveAnswer,
                        CandidateAnswer::getSubjectiveAnswer, CandidateAnswer::getOptionMixSortRule,
                        CandidateAnswer::getForceAnswer, CandidateAnswer::getItemMixSort)
                .eq(CandidateAnswer::getExamId, examId).eq(CandidateAnswer::getCandidateId, candidateId)
                .eq(CandidateAnswer::getPaperId, candidatePaper.getPaperId())
                .eq(CandidateAnswer::getPaperVersion, candidatePaper.getPaperVersion())
                .eq(CandidateAnswer::getTopicId, topicId)
                .orderByAsc(CandidateAnswer::getItemMixSort));
        // 总页数
        Integer pageNumber = paperTopicMapper.selectCount(Wrappers.<PaperTopic>lambdaQuery()
                .eq(PaperTopic::getCandidatePaperId, candidatePaper.getId()));
        boolean isLastPage = pageNumber.intValue() == current.intValue() ? true : false;
        Map<String, CandidateAnswer> candidateAnswerMap = candidateAnswers.stream().collect(
                Collectors.toMap(CandidateAnswer::getItemId, Function.identity(), (k1, k2) -> k1));

        Map<String, PItemDTO> itemDTOMap = iPaperService.getPaperItemByTopicId(candidatePaper.getPaperId(),
                candidatePaper.getPaperVersion(), topicId);
        // 小题列表
        List<PaperQuestionVO> secondItemDataList = new ArrayList<>();
        List<PaperQuestionVO> itemDataList = new ArrayList<>();

        for (CandidateAnswer candidateAnswer : candidateAnswers) {
            PItemDTO pItemDTO = itemDTOMap.get(candidateAnswer.getItemId());
            // 组装大题信息18339
            this.setPaperTopic4New(pItemDTO, paperTopic);
            if (StringUtils.isBlank(candidateAnswer.getObjectiveAnswer())
                    && StringUtils.isBlank(candidateAnswer.getForceAnswer())) {
                // 返回下一页数据
                itemDataList.add(this.getPaperItem4New(pItemDTO)
                        .setItemSort(candidateAnswer.getItemMixSort()));
            } else {
                if (StringUtils.isBlank(candidateAnswer.getObjectiveAnswer())
                        && StringUtils.isNotBlank(candidateAnswer.getForceAnswer())) {
                    ForceItem forceItem = forceItemMapper.selectOne(Wrappers.<ForceItem>lambdaQuery()
                            .eq(ForceItem::getExamId, examId).eq(ForceItem::getCandidateId, candidateId)
                            .eq(ForceItem::getPaperId, candidatePaper.getPaperId())
                            .eq(ForceItem::getPaperVersion, candidatePaper.getPaperVersion())
                            .eq(ForceItem::getParentItemId, candidateAnswer.getItemId())
                            .eq(ForceItem::getDisturbOption, candidateAnswer.getForceAnswer()));
                    // 构建小题信息
                    secondItemDataList.add(this.handleForceOption4New(pItemDTO, forceItem)
                            .setItemSort(candidateAnswer.getItemMixSort()));
                } else if (StringUtils.isNotBlank(candidateAnswer.getObjectiveAnswer())) {
                    if (StringUtils.isNotBlank(candidateAnswer.getForceAnswer())) {
                        ForceItem forceItem = forceItemMapper.selectOne(Wrappers.<ForceItem>lambdaQuery()
                                .eq(ForceItem::getExamId, examId).eq(ForceItem::getCandidateId, candidateId)
                                .eq(ForceItem::getPaperId, candidatePaper.getPaperId())
                                .eq(ForceItem::getPaperVersion, candidatePaper.getPaperVersion())
                                .eq(ForceItem::getParentItemId, candidateAnswer.getItemId())
                                .eq(ForceItem::getDisturbOption, candidateAnswer.getForceAnswer()));
                        // 构建小题信息
                        PaperQuestionVO question =
                                this.handleForceOption4New(pItemDTO, forceItem).setItemSort(candidateAnswer.getItemMixSort());
                        question.getOptionsDataList().forEach(item -> {
                            item.setEdit(false);
                        });
                        question.setUserAnswer(candidateAnswer.getObjectiveAnswer());
                        secondItemDataList.add(question);
                    } else {
                        // 重新登录后，最后一页没有选中干扰项
                        PaperQuestionVO question = this.getPaperItem4New(pItemDTO)
                                .setItemSort(candidateAnswer.getItemMixSort());
                        if (isLastPage) {
                            question.getOptionsDataList().forEach(item -> {
                                item.setEdit(false);
                            });
                            question.setUserAnswer(candidateAnswer.getObjectiveAnswer());
                        }
                        itemDataList.add(question);
                    }
                }
            }
        }

        if (secondItemDataList.size() > 0) {
            // 防止小题号乱序
            paperTopic.setItemData(secondItemDataList.stream()
                    .sorted(Comparator.comparing(PaperQuestionVO::getItemSort))
                    .collect(Collectors.toList()));
        } else {
            // 防止小题号乱序
            paperTopic.setItemData(itemDataList.stream()
                    .sorted(Comparator.comparing(PaperQuestionVO::getItemSort))
                    .collect(Collectors.toList()));
        }

        // 防止选项号乱序
        this.sortItemListOptions4New(candidateAnswerMap, paperTopic);

        redisTemplate.opsForValue().set(KeyDefineCommon.getCandidateNextNeedAnswerNumber(examId, candidateId,
                        candidatePaper.getPaperId(), candidatePaper.getPaperVersion(), topicId),
                paperTopic.getItemData().size(), Constants.time, Constants.timeUnit);
        if (current > 1) {
            // 如果翻页后，前面那一页标识为已已完成，为了重新登录的时候，显示下一页
            paperTopicMapper.updateFinishState(candidatePaper.getId(), current - 1);
        }
    }

    private void sortItemListOptions4New(Map<String, CandidateAnswer> candidateAnswerMap,
                                         PaperTopicItemListVO paperTopic) {
        // 对小题选项进行排序，按照初始化试卷时定义的顺序，重新排序
        if (paperTopic != null && paperTopic.getItemData() != null) {
            paperTopic.getItemData().stream().forEach(item -> {
                String itemType = item.getItemType();
                if (PaperItemTypeCode.ITEM_TYPE_COMPOUND_CODE.getCode().equalsIgnoreCase(itemType)) {
                    paperTopic.getItemData().stream().forEach(childItem -> {
                        this.sortItemOption4New(candidateAnswerMap.get(childItem.getItemId()), childItem);
                    });
                } else {
                    this.sortItemOption4New(candidateAnswerMap.get(item.getItemId()), item);
                }
            });
        }
    }

    public void sortItemOption4New(CandidateAnswer ca, PaperQuestionVO item) {
        item.setItemSort(ca.getItemMixSort());
        String standerAnswer = item.getStandardAnswer();
        // 过滤填空题
        if (!PaperItemTypeCode.ITEM_TYPE_FILL_IN_BLANKS_CODE.getCode().equalsIgnoreCase(item.getItemType())) {
            String optionMixSortRule = ca.getOptionMixSortRule();
            // 除了填空题，其他主观题没有选项
            if (StringUtils.isNotBlank(optionMixSortRule)) {
                String[] arr = optionMixSortRule.split(",");
                List<TopicOptionVO> topicOptionVOList = new ArrayList<>();
                // 判断题
                if (PaperItemTypeCode.ITEM_TYPE_JUDGMENT_CODE.getCode().equalsIgnoreCase(item.getItemType())) {
                    for (String s : arr) {
                        TopicOptionVO topicOptionVO = new TopicOptionVO();
                        topicOptionVO.setOptionId(s);
                        topicOptionVO.setOptionContent("0".equals(s) ? "错" : "对");
                        // 错：0,对：1
                        topicOptionVO.setOptionValue("0".equals(s) ? "0" : "1");
                        item.setStandardAnswer("0".equals(standerAnswer) ? "错" : "对");
                        topicOptionVOList.add(topicOptionVO);
                    }
                } else {
                    // 单选、多选题
                    Map<String, TopicOptionVO> map = item.getOptionsDataList().stream()
                            .collect(Collectors.toMap(TopicOptionVO::getOptionId, option -> option));
                    for (int i = 0; i < arr.length; i++) {
                        String s = arr[i];
                        TopicOptionVO topicOptionVO = map.get(s);
                        if (topicOptionVO == null) {
                            continue;
                        }
                        topicOptionVO.setOptionValue(topicOptionVO.getOptionName());
                        topicOptionVO.setOptionName(Constants.ABC_ARRAY[i]);
                        //标答修改为乱序后的标答
                        if (topicOptionVO.getOptionValue().equals(standerAnswer)) {
                            item.setStandardAnswer(Constants.ABC_ARRAY[i]);
                        }
                        topicOptionVOList.add(topicOptionVO);
                    }
                }
                item.setOptionsDataList(topicOptionVOList);
            }
        }
    }

    private PaperQuestionVO getPaperItem4New(PItemDTO pItemDTO) {
        PaperQuestionVO paperItem = new PaperQuestionVO();
        paperItem.setItemId(pItemDTO.getItemId());
        paperItem.setItemType(pItemDTO.getItemType());
        paperItem.setItemContent(pItemDTO.getItemContent());
        paperItem.setStandardAnswer(pItemDTO.getStandardAnswer());
        paperItem.setDescription(pItemDTO.getItemDescription());
        List<TopicOptionVO> topicOptions = new ArrayList<TopicOptionVO>();
        pItemDTO.getOptionMap().forEach((key, optionsData) -> {
            TopicOptionVO topicOption = new TopicOptionVO();
            topicOption.setOptionId(optionsData.getOptionId());
            topicOption.setOptionName(optionsData.getOptionName());
            topicOption.setOptionContent(optionsData.getOptionContent());
            topicOptions.add(topicOption);
        });
        paperItem.setOptionsDataList(topicOptions);
        return paperItem;
    }

    /**
     * 组装二次迫选数据
     *
     * @param pItemDTO
     * @return
     */
    private PaperQuestionVO handleForceOption4New(PItemDTO pItemDTO, ForceItem forceItem) {
        PaperQuestionVO paperQuestionVO = new PaperQuestionVO();
        paperQuestionVO.setItemId(pItemDTO.getItemId());
        paperQuestionVO.setItemType(pItemDTO.getItemType());
        paperQuestionVO.setItemContent(pItemDTO.getItemContent());
        paperQuestionVO.setStandardAnswer(pItemDTO.getStandardAnswer());
        paperQuestionVO.setDescription(pItemDTO.getItemDescription());
        List<TopicOptionVO> optionVOList = new ArrayList<>();
        paperQuestionVO.setOptionsDataList(optionVOList);
        // 可编辑的选项
        String[] arr = forceItem.getSecondEditOption().split(",");
        for (String s : arr) {
            pItemDTO.getOptionMap().forEach((k, v) -> {
                if (v.getOptionName().equalsIgnoreCase(s)) {
                    // 只需要指定的选项
                    TopicOptionVO optionVO = new TopicOptionVO();
                    optionVO.setOptionId(v.getOptionId());
                    optionVO.setOptionName(s);
                    optionVO.setEdit(true);
                    optionVO.setOptionContent(v.getOptionContent());
                    optionVOList.add(optionVO);
                }
            });
        }
        if (forceItem.getSecondViewOption() != null) {
            // 不可编辑的选项
            arr = forceItem.getSecondViewOption().split(",");
            for (String s : arr) {
                pItemDTO.getOptionMap().forEach((k, v) -> {
                    if (v.getOptionName().equalsIgnoreCase(s)) {
                        // 只需要指定的选项
                        TopicOptionVO optionVO = new TopicOptionVO();
                        optionVO.setOptionId(v.getOptionId());
                        optionVO.setOptionName(s);
                        optionVO.setEdit(false);
                        optionVO.setOptionContent(v.getOptionContent());
                        optionVOList.add(optionVO);
                    }
                });
            }
        }
        return paperQuestionVO;
    }

    private void setPaperTopic4New(PItemDTO itemDTO, PaperTopicItemListVO paperTopic) {
        paperTopic.setTopicId(itemDTO.getTopicId());
        paperTopic.setTopicName(itemDTO.getTopicName());
        paperTopic.setTopicDescription(itemDTO.getTopicDescription());
        paperTopic.setTopicSort(itemDTO.getTopicSort());
        paperTopic.setTopicScore(itemDTO.getTopicScore());
        paperTopic.setItemNumber(itemDTO.getItemNumber());
    }


    @Override
    public List<CandidateTimeLength> getMonitorScreen(String examId) {
        return baseMapper.getMonitorScreen(examId);
    }

    @Override
    public void forceFinishExam(String json) {
        // 更新状态，更新成功，则证明考生在考试端登录过，未登录不需要处理后面的逻辑
        if (StringUtils.isNotBlank(json)) {
            ForceFinishExamDTO param = JSON.parseObject(json, ForceFinishExamDTO.class);
            QueryWrapper<CandidateProcess> query = Wrappers.query();
            query.lambda().select(CandidateProcess::getExamStatus)
                    .eq(CandidateProcess::getCandidateId, param.getCandidateId())
                    .eq(CandidateProcess::getDelFlag, false);
            CandidateProcess candidateProcess = super.getOne(query);
            // 先查询出来试卷
            List<CandidatePaper> list = iCandidatePaperService.lambdaQuery()
                    .select(CandidatePaper::getId, CandidatePaper::getMixSort,
                            CandidatePaper::getPaperId, CandidatePaper::getPaperVersion,
                            CandidatePaper::getFinish, CandidatePaper::getPaperTimeLength,
                            CandidatePaper::getRemainingTime, CandidatePaper::getExtraTime)
                    .eq(CandidatePaper::getExamId, param.getExamId())
                    .eq(CandidatePaper::getCandidateId, param.getCandidateId()).list();

            if (param.getForceFinishExam() != null && param.getForceFinishExam() && candidateProcess != null
                    && candidateProcess.getExamStatus().intValue() < CandidateStatusCode.COMMIT.getCode().intValue()) {
                // 强制结束考试时统分
                CandidateFinishExamVO candidateFinishExamVO = new CandidateFinishExamVO();
                candidateFinishExamVO.setExamStatus(param.getExamStatus());
                candidateFinishExamVO.setExamId(param.getExamId());
                candidateFinishExamVO.setCandidateId(param.getCandidateId());
                candidateFinishExamVO.setPaperList(list);
                iCandidateAnswerService.calcObjectiveScore(candidateFinishExamVO, true);
                //推送成绩系统
                iThirdInterfaceService.pushScoreToCj(param.getExamId(), param.getCandidateId(), null, null);
            }
        }
    }

    @Override
    public AlreadyOverScreenCountVO getAlreadyOverScreenCount(String examId, String candidateId) {
        // 获取考试配置
        List<ExamConfigVO> configList = iExamCacheService.getExamConfigCache(examId);
        Optional<ExamConfigVO> overScreenNumberConfig = configList.stream()
                .filter(c -> ExamConfigCode.EXAM_CONFIG_CODE_OVER_SCREEN_NUM.getCode().equals(c.getConfigCode()))
                .findFirst();
        if (overScreenNumberConfig.isPresent()) {
            QueryWrapper<CandidateProcess> query = Wrappers.query();
            query.lambda().eq(CandidateProcess::getCandidateId, candidateId)
                    .eq(CandidateProcess::getDelFlag, false);
            // 获取必要字段
            CandidateProcess candidateProcess = super.lambdaQuery()
                    .select(CandidateProcess::getId, CandidateProcess::getExamStatus, CandidateProcess::getChangScreenNumber)
                    .eq(CandidateProcess::getExamId, examId).eq(CandidateProcess::getCandidateId, candidateId)
                    .eq(CandidateProcess::getDelFlag, false).one();
            if (candidateProcess != null
                    && candidateProcess.getExamStatus().intValue() < CandidateStatusCode.COMMIT.getCode().intValue()) {
                Integer changeScreenNumber = candidateProcess.getChangScreenNumber() == null ? 0
                        : candidateProcess.getChangScreenNumber();
                changeScreenNumber++;
                CandidateProcess updateData = new CandidateProcess();
                updateData.setChangScreenNumber(changeScreenNumber);
                updateData.setId(candidateProcess.getId());
                //更新比要字段
                super.updateById(updateData);
                AlreadyOverScreenCountVO alreadyOverScreenCount = new AlreadyOverScreenCountVO();
                alreadyOverScreenCount.setAlreadyOverScreenCount(changeScreenNumber);
                return alreadyOverScreenCount;
            }
        }
        return new AlreadyOverScreenCountVO();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void addExtraTimeByBatch(String json) {
        LocalDateTime now = LocalDateTime.now();
        CandidateAddTimeDTO addTimeInfoDTO = JSON.parseObject(json, CandidateAddTimeDTO.class);
        boolean flag = addTimeInfoDTO.getPaperAddTimeList() != null && addTimeInfoDTO.getPaperAddTimeList().size() > 0;
        if (flag && addTimeInfoDTO.getCandidateIdList().size() > 0) {
            // 试卷包 部分考生 加时
            addTimeInfoDTO.getCandidateIdList().stream().forEach(candidateId -> {
                this.addTimeForCandidate(addTimeInfoDTO, candidateId, now);
            });
        } else {
            // 所有考生 一把更新
            addTimeInfoDTO.getPaperAddTimeList().stream().forEach(paper -> {
                // 转为毫秒
                long time = paper.getTimeLength() * 60 * 1000;
                iCandidatePaperService.addTimeByBatch(addTimeInfoDTO.getExamId(), paper.getPaperId(),
                        paper.getPaperVersion(), time);
                List<String> candidateIdList = iCandidatePaperService.getAddedTimeCandidate(addTimeInfoDTO.getExamId(),
                        paper.getPaperId(), paper.getPaperVersion());
                candidateIdList.forEach(candidateId -> {
                    kwClient.updateCandidateAddTimeState(candidateId);
                    iLogService.addTimeLog(addTimeInfoDTO.getExamId(), candidateId, paper.getPaperId(), paper.getPaperVersion(),
                            paper.getTimeLength(), addTimeInfoDTO.getUserId(), now);
                });

            });
        }
    }

    private void addTimeForCandidate(CandidateAddTimeDTO addTimeInfoDTO, String candidateId, LocalDateTime now) {
        // 查询考生状态为未完成考试，即<30，且试卷状态is_finish为FALSE未交卷
        addTimeInfoDTO.getPaperAddTimeList().stream().forEach(paper -> {
            if (paper.getTimeLength().longValue() != 0) {
                CandidatePaper candidatePaper = iCandidatePaperService.getCandidatePaper(addTimeInfoDTO.getExamId(),
                        candidateId, paper.getPaperId(), paper.getPaperVersion());
                if (candidatePaper != null) {
                    // 转成毫秒
                    long time = paper.getTimeLength() * 60 * 1000;
                    CandidatePaper updateEntity = new CandidatePaper();
                    updateEntity.setId(candidatePaper.getId());
                    updateEntity.setExtraTime(candidatePaper.getExtraTime() + time);
                    updateEntity.setRemainingTime(candidatePaper.getRemainingTime());
                    iCandidatePaperService.updateById(updateEntity);
                    kwClient.updateCandidateAddTimeState(candidateId);
                    iLogService.addTimeLog(addTimeInfoDTO.getExamId(), candidateId, paper.getPaperId(),
                            paper.getPaperVersion(), paper.getTimeLength(), addTimeInfoDTO.getUserId(), now);
                }
            }
        });
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void resetLoginCount(String examId, String candidateId, String userId) {
        super.lambdaUpdate()
                .eq(CandidateProcess::getExamId, examId).eq(CandidateProcess::getCandidateId, candidateId)
                .set(CandidateProcess::getLoginCount, 0).update();
        iLogService.resetLoginLog(examId, candidateId, userId);
    }

    @Override
    public void change2Testing(String examId, String candidateId) {
        LambdaUpdateWrapper<CandidateProcess> update = Wrappers.<CandidateProcess>lambdaUpdate()
                .set(CandidateProcess::getExamStatus, CandidateStatusCode.TESTING.getCode())
                .eq(CandidateProcess::getExamId, examId)
                .eq(CandidateProcess::getCandidateId, candidateId);
        // 更新考生未考中状态
        super.update(update);
        CandidateInfoUpdateDTO candidateInfoUpdateVO = CandidateInfoUpdateDTO.builder()
                .examId(examId).candidateId(candidateId)
                .state(CandidateStatusCode.TESTING.getCode()).build();
        // 通知考务更新考生为考中状态
        this.updateKwCandidateInfo(candidateInfoUpdateVO);
    }

    @Override
    public List<CancelCommitPaper> getCandidateAllPaperList(String examId, String candidateId) {
        List<CancelCommitPaper> papers = new ArrayList<>();
        List<CandidatePaper> list = iCandidatePaperService.list(Wrappers.<CandidatePaper>lambdaQuery()
                .eq(CandidatePaper::getExamId, examId).eq(CandidatePaper::getCandidateId, candidateId)
                .orderByAsc(CandidatePaper::getMixSort));
        for (CandidatePaper candidatePaper : list) {
            CancelCommitPaper cp = new CancelCommitPaper();
            cp.setId(candidatePaper.getId());
            cp.setPaperType(candidatePaper.getPaperType());
            cp.setPaperId(candidatePaper.getPaperId());
            cp.setPaperVersion(candidatePaper.getPaperVersion());
            long remainingTime = this.getRemainTime(candidatePaper);
            cp.setRemainingTime(remainingTime);
            cp.setAllowEdit(true);
            papers.add(cp);
        }
        return papers;
    }

    @Override
    public void clearInitPaperDataByCandidateId(Integer examCode, String zkz) {
        ApiResponse<String> res = kwClient.getExamIdByCode(examCode);
        if (StringUtils.isNotBlank(res.getData())) {
            String examId = res.getData();
            CandidateProcess one = super.lambdaQuery()
                    .select(CandidateProcess::getCandidateId, CandidateProcess::getId)
                    .eq(CandidateProcess::getExamId, examId).eq(CandidateProcess::getAdmissionNumber, zkz)
                    .eq(CandidateProcess::getDelFlag, false)
                    .last("limit 1").one();
            if (one != null) {
                this.clearInitPaperDataByCandidateId(one.getId(), examId, one.getCandidateId());
            }
        }
    }

    @Override
    public void clearInitPaperDataByCandidateId(String candidateId) {
        CandidateProcess one = super.lambdaQuery()
                .select(CandidateProcess::getExamId, CandidateProcess::getId)
                .eq(CandidateProcess::getCandidateId, candidateId)
                .eq(CandidateProcess::getDelFlag, false)
                .last("limit 1").one();
        if (one != null) {
            this.clearInitPaperDataByCandidateId(one.getId(), one.getExamId(), candidateId);
        }
    }

    @Transactional
    public void clearInitPaperDataByCandidateId(String processId, String examId, String candidateId) {

        removeById(processId);

        iCandidateAnswerService.remove(Wrappers.<CandidateAnswer>lambdaQuery()
                .eq(CandidateAnswer::getExamId, examId)
                .eq(CandidateAnswer::getCandidateId, candidateId));

        iCandidatePaperService.remove(Wrappers.<CandidatePaper>lambdaQuery()
                .eq(CandidatePaper::getExamId, examId)
                .eq(CandidatePaper::getCandidateId, candidateId));

    }

    @Override
    public void revokeToken(String accessToken) {
        String oauthClientId = configManager.getOauthClientId();
        String oauthClientSecret = configManager.getOauthClientSecret();
        loginClient.revokeToken(accessToken, oauthClientId, oauthClientSecret);
    }

    @Override
    public void overScreenLog(OverScreenLogVO overScreenLogVO) {
        kwClient.overScreenLog(overScreenLogVO);
    }

    @Override
    public void setPassCandidatePhotoVerification(String examId, String candidateId) {
        CandidateProcess candidateProcess = new CandidateProcess();
        candidateProcess.setTakePicture(true);
        candidateProcess.setCheckedIdCard(true);
        QueryWrapper<CandidateProcess> wrapper = new QueryWrapper<CandidateProcess>();
        wrapper.lambda().eq(CandidateProcess::getCandidateId, candidateId).eq(CandidateProcess::getExamId, examId);
        super.update(candidateProcess, wrapper);
        //写入轨迹

    }

    @Override
    public String getExamCandidateByAdmissionNumber(String examId, String admissionNumber) {
        try {
            if (Objects.isNull(admissionNumber)) {
                return null;
            }
            ApiResponse<LoginInfo> clientResponse = kwClient.getCandidateAdmissionNumber(examId, admissionNumber, "3");
            // 判断考务是否有考生
            if (clientResponse.getCode().intValue() != BizStatusCode.KW_SUCCESS_OK.getCode().intValue()) {
                return clientResponse.getData().getCandidateId();
            }
        } catch (Exception e) {
            log.error("考试{}获取考生{}信息失败", examId, admissionNumber);
        }
        return null;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public FaceAuthFailVO faceVerifyFail(FaceAuthFailParam faceAuthFailParam) {
        // 查询数据库记录的人脸核身验证失败次数
        CandidateProcess candidateProcess = lambdaQuery()
                .eq(CandidateProcess::getExamId, faceAuthFailParam.getExamId())
                .eq(CandidateProcess::getCandidateId, faceAuthFailParam.getCandidateId())
                .eq(CandidateProcess::getDelFlag, 0)
                .orderByDesc(CandidateProcess::getCreateDate)
                .select(BaseEntity::getId, CandidateProcess::getFaceNum)
                .last("limit 1").one();
        if (Objects.isNull(candidateProcess)) {
            throw new KsException(NOT_EXIST_CANDIDATE.getCode(), NOT_EXIST_CANDIDATE.getMsg());
        }

        // 计算新的人脸核身验证失败次数
        Integer faceNum = candidateProcess.getFaceNum();
        faceNum = faceNum == null ? 0 : faceNum;
        int newFaceNum = faceNum + 1;

        // 更新新的人脸核身验证失败次数
        LocalDateTime now = LocalDateTime.now();
        boolean updateSuccess = lambdaUpdate()
                .eq(BaseEntity::getId, candidateProcess.getId())
                .set(CandidateProcess::getFaceNum, newFaceNum)
                .set(BaseEntity::getUpdateDate, now)
                .set(BaseEntity::getUpdateBy, faceAuthFailParam.getCandidateId())
                .update();
        if (!updateSuccess) {
            throw new RuntimeException("人脸核身验证失败次数更新失败");
        }
        iLogService.faceFailLog(faceAuthFailParam.getExamId(), faceAuthFailParam.getCandidateId());
        List<ExamConfigVO> examConfigList = iExamCacheService.getExamConfigCache(faceAuthFailParam.getExamId());
        Optional<ExamConfigVO> examConfigApplyFace = examConfigList.stream().filter(e -> e.getConfigCode().equals(ExamConfigCode.EXAM_CONFIG_CODE_APPLY_PASS_AUTHENTICATION.getCode())).findFirst();
        Boolean applyFace = examConfigApplyFace.isPresent() && examConfigApplyFace.get().getConfigValue().equals("true");
        Integer faceSkipVerifyNum = faceVerifyConfig.getFaceSkipVerifyNum();
        if(applyFace) {
            Optional<ExamConfigVO> examConfigApplyFaceFaildCount = examConfigList.stream().filter(e -> e.getConfigCode().equals(ExamConfigCode.EXAM_CONFIG_CODE_AUTHENTICATION_FAILD_COUNT.getCode())).findFirst();
            if(examConfigApplyFaceFaildCount.isPresent() && StringUtils.isNotBlank(examConfigApplyFaceFaildCount.get().getConfigValue())) {
                faceSkipVerifyNum = Integer.parseInt(examConfigApplyFaceFaildCount.get().getConfigValue());
            }
        }

        // 计算是否跳过人脸 跳过时记录下人脸跳过审核
        boolean isSkip = newFaceNum >= faceSkipVerifyNum;

        if (isSkip && !applyFace) {
            ApplyCheck dbApplyCheck = applyCheckService.getCandidateApplyCheck(faceAuthFailParam.getExamId(),
                    faceAuthFailParam.getCandidateId(), LogEventType.OPERATOR_LOG.getCode(),
                    CandidateOperatorLogType.ID_FACE_SKIP_APPLY.getCode(),
                    ApplyCheckState.WAIT_HANDLE.getCode()
            );
            if (Objects.isNull(dbApplyCheck)) {
                // 数据库不存在待审核的数据则创建
                ApplyCheck applyCheck = new ApplyCheck()
                        .setId(UUIDUtils.randomUUID())
                        .setExamId(faceAuthFailParam.getExamId())
                        .setCandidateId(faceAuthFailParam.getCandidateId())
                        .setEventType(LogEventType.OPERATOR_LOG.getCode())
                        .setOperateType(CandidateOperatorLogType.ID_FACE_SKIP_APPLY.getCode())
                        .setState(ApplyCheckState.WAIT_HANDLE.getCode())
                        .setCreateDate(now)
                        .setUpdateDate(now);
                log.info("新增学生人脸核身跳过申请：{} {}", faceAuthFailParam, now);
                boolean saveSuccess = applyCheckService.save(applyCheck);
                if (!saveSuccess) {
                    throw new RuntimeException(String.format("人脸核身验证失败次数已达到%s，但保存审核信息失败", faceVerifyConfig.getFaceSkipVerifyNum()));
                }
                // 更新人脸核身跳过到kw
                KwExamCandidate examCandidate = new KwExamCandidate();
                examCandidate.setId(faceAuthFailParam.getCandidateId());
                examCandidate.setFaceState(FaceState.FAILD.getCode());
                log.info("更新人脸核身失败到kw：{}", examCandidate);
                kwClient.updateCandidate(examCandidate);
            } else {
                // 存在待审核数据时更新时间
                applyCheckService.lambdaUpdate()
                        .eq(ApplyCheck::getExamId, faceAuthFailParam.getExamId())
                        .eq(ApplyCheck::getCandidateId, faceAuthFailParam.getCandidateId())
                        .eq(ApplyCheck::getEventType, LogEventType.OPERATOR_LOG.getCode())
                        .eq(ApplyCheck::getOperateType, CandidateOperatorLogType.ID_FACE_SKIP_APPLY.getCode())
                        .set(ApplyCheck::getUpdateDate, now)
                        .update();
                log.info("更新学生人脸核身跳过申请：{} {}", faceAuthFailParam, now);
            }
        }
        return new FaceAuthFailVO().setFaceSkipApply(isSkip).setFaceNum(newFaceNum);
    }

    @Override
    public void dataCheck(String examId) {
        String resultErrorCode = "";
        String resultComment = "";
        //考生状态是否一致
        Integer vaildCandidateState = this.vaildCandidateState(examId);
        if(vaildCandidateState.intValue() == 1) {
            resultErrorCode += "001,";
        }
        //验证因故未完成有答案
        Integer noCompleteExamByResonHasAnswer = baseMapper.noCompleteExamByResonHasAnswer(examId);
        if(vaildCandidateState.intValue() > 0) {
            resultErrorCode += "002,";
            resultComment += "\n验证因故未完成有答案验证不通过";
            resultComment += "\nselect count(1) from (\n" +
                    "        select kcp.exam_id,kca.candidate_id\n" +
                    "        from ks_candidate_answer kca\n" +
                    "        left join ks_candidate_process kcp on kcp.candidate_id = kca.candidate_id and kcp.exam_id = kca.exam_id\n" +
                    "        where (objective_answer is not null OR subjective_answer is not null ) and kcp.exam_status=50\n" +
                    "        and kca.exam_id = '" + examId + "'" +
                    "        group by kca.candidate_id,kcp.exam_id\n" +
                    "        ) a;";
        }
        //验证已完成考试未初始化试卷
        Integer finishExamUnInitPaper = baseMapper.finishExamUnInitPaper(examId);
        if(finishExamUnInitPaper.intValue() > 0) {
            resultErrorCode += "003,";
            resultComment += "\n验证已完成考试未初始化试卷不通过";
            resultComment += "\n select count(1)\n" +
                    "        from ks_candidate_process\n" +
                    "        where exam_status=30 and paper_id is null\n" +
                    "        and exam_id = '" + examId + "';";
        }
        //验证图片答案题筛查
        Integer pictureAnswerCheck = baseMapper.pictureAnswerCheck(examId);
        if (pictureAnswerCheck.intValue() > 0) {
            resultErrorCode += "004,";
            resultComment += "\n验证图片答案题筛查不通过";
            resultComment += "\n select count(1) from ks_candidate_answer a  \n" +
                    "        where item_type='picture_answer' and\n" +
                    "        a.exam_id = '" + examId + "'" +
                    "        and subjective_answer like '%,';";
        }
        //验证无标答
        Integer noStandardAnswer = baseMapper.noStandardAnswer(examId);
        if (noStandardAnswer.intValue() > 0) {
            resultErrorCode += "005,";
            resultComment += "\n验证无标答不通过";
            resultComment += "\n select count(1)\n" +
                    "        from ks_candidate_answer a\n" +
                    "        where is_objective_or_subjective=1 and (standard_answer is null or standard_answer='')\n" +
                    "        and item_type<>'matching' and item_type<>'cloze_fill' and a.exam_id = '" + examId + "'" +
                    "        ORDER BY a.create_date DESC;";
        }
        //验证客观题答案包含http
        Integer objectiveHasHttpError = baseMapper.objectiveHasHttpError(examId);
        if (objectiveHasHttpError.intValue() > 0) {
            resultErrorCode += "006,";
            resultComment += "\n验证客观题答案包含http不通过";
            resultComment += "\n select count(1)\n" +
                    "        from ks_candidate_answer a\n" +
                    "        where is_objective_or_subjective=1 and objective_answer like 'http%'\n" +
                    "        and a.exam_id = '" + examId + "';";
        }
        //验证简答题包含http
        Integer shortAnswerHasHttpError = baseMapper.shortAnswerHasHttpError(examId);
        if (shortAnswerHasHttpError.intValue() > 0) {
            resultErrorCode += "007,";
            resultComment += "\n验证简答题包含http不通过";
            resultComment += "\n select count(1)\n" +
                    "        from ks_candidate_answer a\n" +
                    "        where item_type='short_answer'  and subjective_answer like '%http%'\n" +
                    "        and a.exam_id = '" + examId + "';";
        }
        //验证填空题包含http
        Integer fillBankAnswerHasHttpError = baseMapper.fillBankAnswerHasHttpError(examId);
        if (fillBankAnswerHasHttpError.intValue() > 0) {
            resultErrorCode += "008,";
            resultComment += "\n验证填空题包含http不通过";
            resultComment += "\n   select count(1)\n" +
                    "        from ks_candidate_answer a\n" +
                    "        where item_type='fill_in_blanks' and parent_item_id &lt;&gt; '' and parent_item_id is not null\n" +
                    "        and subjective_answer like '%http%'\n" +
                    "        and a.exam_id = '" + examId + "';";
        }
        //验证单选或多选题包含数字
        Integer singleOrMultipleHasNum = baseMapper.singleOrMultipleHasNum(examId);
        if (singleOrMultipleHasNum.intValue() > 0) {
            resultErrorCode += "009,";
            resultComment += "\n验证填空题包含http不通过";
            resultComment += "\n select count(1)\n" +
                    "        from ks_candidate_answer a\n" +
                    "        where is_objective_or_subjective=1 and objective_answer like '1%'\n" +
                    "        and a.exam_id = '" + examId + "';" +
                    "        and (item_type='multiple' or item_type='single' or item_type='indefinite') ";
        }
        //验证单选题长度答案大于1
        Integer singleAnswerLengthMoreThanOne = baseMapper.singleAnswerLengthMoreThanOne(examId);
        if (singleAnswerLengthMoreThanOne.intValue() > 0) {
            resultErrorCode += "010,";
            resultComment += "\n验证单选题长度答案大于1不通过";
            resultComment += "\n  select count(1)\n" +
                    "        from ks_candidate_answer a\n" +
                    "        where is_objective_or_subjective=1 and LENGTH(objective_answer)>1\n" +
                    "        and a.exam_id = '" + examId + "'" +
                    "        and item_type='single'";
        }
        //验证多选题答案错误
        Integer multipleAnswerError = baseMapper.multipleAnswerError(examId);
        if (singleAnswerLengthMoreThanOne.intValue() > 0) {
            resultErrorCode += "011,";
            resultComment += "\n验证单选题长度答案大于1不通过";
            resultComment += "\n select count(1)\n" +
                    "        from ks_candidate_answer a\n" +
                    "        where  (item_type='multiple' or item_type='indefinite') and objective_answer not in ('A','B','C','D','E','A,B','A,C','A,D','A,E','B,C','B,D','B,E','C,D','C,E','D,E','A,B,C','A,B,D','A,B,E','A,C,D','A,C,E','A,D,E','B,C,D','B,C,E','B,D,E','C,D,E','A,B,C,D','A,B,C,E','A,B,D,E','A,C,D,E','B,C,D,E','A,B,C,D,E')\n" +
                    "        and objective_answer is not null and objective_answer != ''\n" +
                    "        and a.exam_id = '" + examId + "';";
        }
        //错误答案有得分
        Integer errorAnswerHasScore = baseMapper.errorAnswerHasScore(examId);
        if (errorAnswerHasScore.intValue() > 0) {
            resultErrorCode += "012,";
            resultComment += "\n验证误答案有得分不通过";
            resultComment += "\n  select count(1)\n" +
                    "        from ks_candidate_answer\n" +
                    "        where is_objective_or_subjective=1 and standard_answer  &lt;&gt; objective_answer and score &gt; 0\n" +
                    "        and  exam_id = '" + examId + "' and item_type &lt;&gt; 'indefinite'";
        }
        //正确答案无得分
        Integer correctAnswerHasNotScore = baseMapper.correctAnswerHasNotScore(examId);
        if (correctAnswerHasNotScore.intValue() > 0) {
            resultErrorCode += "013,";
            resultComment += "\n验证正确答案无得分不通过";
            resultComment += "\n select count(1)\n" +
                    "        from ks_candidate_answer\n" +
                    "        where is_objective_or_subjective=1 and standard_answer=objective_answer and (score is null or score=0)  and item_type  &lt;&gt; 'fill_in_blanks'  and item_type  &lt;&gt; 'matching'\n" +
                    "        and  exam_id = '" + examId + "';";
        }
        //空答案有分
        Integer nullAnswerHasScore = baseMapper.nullAnswerHasScore(examId);
        if (nullAnswerHasScore.intValue() > 0) {
            resultErrorCode += "014,";
            resultComment += "\n验证空答案有分不通过";
            resultComment += "\n select count(1)\n" +
                    "        from ks_candidate_answer\n" +
                    "        where is_objective_or_subjective=1 and standard_answer=objective_answer and (score is null or score=0)  and item_type  &lt;&gt; 'fill_in_blanks'  and item_type  &lt;&gt; 'matching'\n" +
                    "        and  exam_id = '" + examId + "';";
        }
        //验证判断题答案错误
        Integer judgementAnswerError = baseMapper.judgementAnswerError(examId);
        if(judgementAnswerError.intValue() > 0) {
            resultErrorCode += "015,";
            resultComment += "\n验证判断题答案错误不通过";
            resultComment += "\n select count(1)\n" +
                    "        from ks_candidate_answer a\n" +
                    "        where item_type='judgment' and objective_answer &lt;&gt; '0' and objective_answer &lt;&gt; '1' and objective_answer &lt;&gt; ''\n" +
                    "        and objective_answer is not null\n" +
                    "        and a.exam_id = '" + examId + "';";
        }
        //验证考生试卷表未统分
        Integer unStatisticsScore = baseMapper.unStatisticsScore(examId);
        if(unStatisticsScore.intValue() > 0) {
            resultErrorCode += "016,";
            resultComment += "\n验证考生试卷表未统分不通过";
            resultComment += "\n select count(1)\n" +
                    "        from ks_candidate_paper\n" +
                    "        where exam_id = '" + examId + "'\n" +
                    "        and objective_score is null";
        }
        //验证过程表分数与答案表总分不一致
        Integer errorScore = baseMapper.errorScore(examId);
        if(errorScore.intValue() > 0) {
            resultErrorCode += "017,";
            resultComment += "\n验证过程表分数与答案表总分不一致不通过";
            resultComment += "\n    select count(1)\n" +
                    "        from ks_candidate_process a,(\n" +
                    "        select sum(score) fs,admission_number,exam_id,candidate_id from ks_candidate_answer\n" +
                    "        where exam_id = '" + examId + "'\n" +
                    "        GROUP BY exam_id,candidate_id,admission_number\n" +
                    "        ) b\n" +
                    "        where a.exam_id=b.exam_id and a.candidate_id=b.candidate_id and a.objective_score!=b.fs";
        }
        ExamDataCheckVO examDataCheck = new ExamDataCheckVO();
        if (StringUtils.isNotBlank(resultErrorCode)) {
            examDataCheck.setCheckResult(3);
            examDataCheck.setCheckResultErrorCode(resultErrorCode.substring(1,resultErrorCode.length()-1));
            examDataCheck.setCheckResultComment(resultComment);
        } else {
            examDataCheck.setCheckResult(1);
        }
        examDataCheck.setExamId(examId);
        kwClient.rewriteDataCheck(examDataCheck);
        //return examDataCheck;
    }

    private Integer vaildCandidateState(String examId) {
        LambdaQueryWrapper<CandidateProcess> queryCandidateProcess = Wrappers.lambdaQuery();
        queryCandidateProcess.select(CandidateProcess::getExamStatus)
                .eq(CandidateProcess::getExamId, examId);
        List<CandidateProcess> candidateProcessList = super.list(queryCandidateProcess);
        Long commitCount = candidateProcessList.stream().filter(c -> c.getExamStatus().intValue() == CandidateStatusCode.COMMIT.getCode().intValue()).count();
        Long noComoleteByReasonCount = candidateProcessList.stream().filter(c -> c.getExamStatus().intValue() == CandidateStatusCode.NOT_COMPLETE_BY_REASON.getCode().intValue()).count();
        CandidateStateCountVO candidateStateCount = kwClient.getCandidateStateCount(examId).getData();
        if(candidateStateCount.getCommitCount().intValue() != commitCount.intValue() || candidateStateCount.getNoComoleteByReasonCount().intValue() != noComoleteByReasonCount.intValue()) {
           return  1;
        }
        return  0;
    }

}
